Merge remote-tracking branch 'origin/1.9.x' into chore-remove-shared-v1

This commit is contained in:
Jake Barnby 2026-04-20 18:04:26 +12:00
commit db3d00b1da
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
112 changed files with 2458 additions and 1315 deletions

View file

@ -509,7 +509,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \

View file

@ -2,10 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));

View file

@ -9,11 +9,5 @@ return [
'mfaChallenge',
'sessionAlert',
'otpSession'
],
'sms' => [
'verification',
'login',
'invitation',
'mfaChallenge'
]
];

View file

@ -1487,13 +1487,13 @@ return [
]
],
[
'key' => 'crm-dashboard-react-admin',
'name' => 'CRM dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with CRM features.',
'key' => 'dashboard-react-admin',
'name' => 'E-commerce dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [SiteUseCases::DASHBOARD],
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/react-admin',

View file

@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''

View file

@ -474,19 +474,26 @@ Http::delete('/v1/account')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
->inject('authorization')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if ($project->getId() === 'console') {
// get all memberships
$memberships = $user->getAttribute('memberships', []);
foreach ($memberships as $membership) {
// prevent deletion if at least one active membership
if ($membership->getAttribute('confirm', false)) {
throw new Exception(Exception::USER_DELETION_PROHIBITED);
if (!$membership->getAttribute('confirm', false)) {
continue;
}
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
if ($team->isEmpty()) {
continue;
}
// Team is left as-is — we don't promote non-owner members to owner.
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
}
}
@ -2265,7 +2272,10 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $locale->getText("emails.magicSession.subject");
$preview = $locale->getText("emails.magicSession.preview");
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$agentOs = $detector->getOS();
@ -2575,7 +2585,9 @@ Http::post('/v1/account/tokens/email')
$preview = $locale->getText("emails.otpSession.preview");
$heading = $locale->getText("emails.otpSession.heading");
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();
@ -2968,11 +2980,6 @@ Http::post('/v1/account/tokens/phone')
if ($sendSMS) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$projectName = $project->getAttribute('name');
if ($project->getId() === 'console') {
$projectName = $platform['platformName'];
@ -3726,7 +3733,9 @@ Http::post('/v1/account/recovery')
$body = $locale->getText("emails.recovery.body");
$subject = $locale->getText("emails.recovery.subject");
$preview = $locale->getText("emails.recovery.preview");
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
$message
@ -4034,7 +4043,9 @@ Http::post('/v1/account/verifications/email')
$subject = $locale->getText("emails.verification.subject");
$heading = $locale->getText("emails.verification.heading");
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();
@ -4333,11 +4344,6 @@ Http::post('/v1/account/verifications/phone')
if ($sendSMS) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
$messageContent
->setParam('{{project}}', $project->getAttribute('name'))

View file

@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {

View file

@ -833,75 +833,8 @@ Http::post('/v1/projects/:projectId/smtp/tests')
$response->noContent();
});
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Get custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'templates',
name: 'getSmsTemplate',
description: '/docs/references/projects/get-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.getSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'templates',
name: 'getSMSTemplate',
description: '/docs/references/projects/get-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
$template = [
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
];
}
$template['type'] = $type;
$template['locale'] = $locale;
$response->dynamic(new Document($template), Response::MODEL_SMS_TEMPLATE);
});
Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
Http::get('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Get custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -920,10 +853,12 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
->inject('locale')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);
@ -1000,74 +935,8 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
});
Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Update custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'templates',
name: 'updateSmsTemplate',
description: '/docs/references/projects/update-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'templates',
name: 'updateSMSTemplate',
description: '/docs/references/projects/update-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('message', '', new Text(0), 'Template message')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForPlatform) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$templates['sms.' . $type . '-' . $locale] = [
'message' => $message
];
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'message' => $message,
'type' => $type,
'locale' => $locale,
]), Response::MODEL_SMS_TEMPLATE);
});
Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
Http::patch('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Update custom email templates')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1086,7 +955,7 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->param('subject', '', new Text(255), 'Email Subject')
->param('message', '', new Text(0), 'Template message')
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
@ -1094,7 +963,9 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
->param('replyTo', '', new Email(), 'Reply to email', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform) {
->inject('locale')
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);
@ -1124,79 +995,8 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
]), Response::MODEL_EMAIL_TEMPLATE);
});
Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Reset custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'templates',
name: 'deleteSmsTemplate',
description: '/docs/references/projects/delete-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
],
contentType: ContentType::JSON,
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.deleteSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'templates',
name: 'deleteSMSTemplate',
description: '/docs/references/projects/delete-sms-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SMS_TEMPLATE,
)
],
contentType: ContentType::JSON
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
}
unset($template['sms.' . $type . '-' . $locale]);
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'message' => $template['message']
]), Response::MODEL_SMS_TEMPLATE);
});
Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
Http::delete('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Delete custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
@ -1216,10 +1016,12 @@ Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
->inject('locale')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);

View file

@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@ -1014,11 +1014,11 @@ Http::init()
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('platform')
->inject('authorization')
->inject('certifiedDomains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
@ -1044,7 +1044,7 @@ Http::init()
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
@ -1100,10 +1100,11 @@ Http::init()
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: $document,
skipRenewCheck: true,
));
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {

View file

@ -3,15 +3,17 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@ -88,7 +90,7 @@ Http::init()
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('auditContext')
->inject('project')
->inject('user')
->inject('session')
@ -97,7 +99,7 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
@ -193,7 +195,7 @@ Http::init()
'name' => $apiKey->getName(),
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
// For standard keys, update last accessed time
@ -264,7 +266,7 @@ Http::init()
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
default => ACTIVITY_TYPE_KEY_PROJECT,
});
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
// Apply permission
@ -486,7 +488,7 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@ -503,7 +505,7 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
@ -596,13 +598,12 @@ Http::init()
->setProject($project)
->setUser($user);
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
$auditContext->mode = $mode;
$auditContext->userAgent = $request->getUserAgent('');
$auditContext->ip = $request->getIP();
$auditContext->hostname = $request->getHostname();
$auditContext->event = $route->getLabel('audits.event', '');
$auditContext->project = $project;
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
@ -611,7 +612,7 @@ Http::init()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
/* Auto-set projects */
@ -790,7 +791,8 @@ Http::shutdown()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('auditContext')
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
@ -807,7 +809,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@ -902,7 +904,7 @@ Http::shutdown()
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (! empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
$auditContext->resource = $resource;
}
}
@ -912,8 +914,8 @@ Http::shutdown()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
@ -931,24 +933,21 @@ Http::shutdown()
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
$auditContext->payload = $responsePayload;
}
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
@ -972,7 +971,8 @@ Http::shutdown()
if ($useCache) {
$resource = $resourceType = null;
$data = $response->getPayload();
if (! empty($data['payload'])) {
$statusCode = $response->getStatusCode();
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
$pattern = $route->getLabel('cache.resource', null);
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);

View file

@ -72,8 +72,6 @@ $swooleAdapter = new Server(
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
@ -525,6 +523,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);

View file

@ -117,7 +117,9 @@ use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@ -135,7 +137,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSite;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
@ -190,8 +191,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
Response::setModel(new ProviderRepositoryFrameworkList());
Response::setModel(new ProviderRepositoryRuntimeList());
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
@ -373,7 +374,6 @@ Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Response::setModel(new MFAChallenge());

View file

@ -1,8 +1,11 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Utopia\Database\Documents\User;
@ -81,6 +84,18 @@ $container->set('publisherMessaging', function (Publisher $publisher) {
$container->set('publisherWebhooks', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))

View file

@ -4,9 +4,8 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit as AuditEvent;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@ -14,7 +13,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
@ -128,9 +126,6 @@ return function (Container $container): void {
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
@ -149,18 +144,13 @@ return function (Container $container): void {
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('queueForAudits', function (Publisher $publisher) {
return new AuditEvent($publisher);
}, ['publisher']);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);

View file

@ -1,8 +1,6 @@
<?php
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@ -10,7 +8,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@ -311,10 +308,6 @@ return function (Container $container): void {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
@ -323,10 +316,6 @@ return function (Container $container): void {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
@ -339,10 +328,6 @@ return function (Container $container): void {
return new Realtime();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);

View file

@ -398,6 +398,11 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
name: 'realtime.server.delivery_delay',
unit: 'ms',
advisory: ['ExplicitBucketBoundaries' => [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000]],
));
$attempts = 0;
$start = time();
@ -592,6 +597,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
if (\is_string($updatedAt)) {
try {
$updatedAtDate = new \DateTimeImmutable($updatedAt);
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
$nowTimestampMs = (float) $now->format('U.u') * 1000;
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
} catch (\Throwable) {
// Ignore invalid timestamp payloads.
}
}
$projectId = $event['project'] ?? null;

View file

@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-executions:
@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-executions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
<?php if ($enableAssistant): ?>
appwrite-assistant:
@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-mongodb:/data/db
- appwrite-mongodb-keyfile:/data/keyfile
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
@ -1205,7 +1185,6 @@ volumes:
<?php elseif ($dbService === 'mongodb'): ?>
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-mongodb-config:
<?php endif; ?>
appwrite-redis:
appwrite-cache:

View file

@ -74,7 +74,7 @@
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.12.*",
"utopia-php/platform": "0.13.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
@ -92,7 +92,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},

100
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "c5ae97637fd0ec0a950044d1c33677ea",
"packages": [
{
"name": "adhocore/jwt",
@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.20",
"version": "5.3.21",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"shasum": ""
},
"require": {
@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.20"
"source": "https://github.com/utopia-php/database/tree/5.3.21"
},
"time": "2026-04-10T08:27:41+00:00"
"time": "2026-04-10T12:38:57+00:00"
},
{
"name": "utopia-php/detector",
@ -4325,16 +4325,16 @@
},
{
"name": "utopia-php/image",
"version": "0.8.4",
"version": "0.8.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65"
"reference": "9af2fcff028a42550465e2ccad88e3b31c3584f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/ce788ff0121a79286fdbe3ef3eba566de646df65",
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65",
"url": "https://api.github.com/repos/utopia-php/image/zipball/9af2fcff028a42550465e2ccad88e3b31c3584f3",
"reference": "9af2fcff028a42550465e2ccad88e3b31c3584f3",
"shasum": ""
},
"require": {
@ -4343,10 +4343,12 @@
"php": ">=8.1"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.10.0",
"phpunit/phpunit": "^9.3",
"vimeo/psalm": "4.13.1"
"laravel/pint": "1.24.*",
"phpstan/phpstan": "2.1.*",
"phpunit/phpunit": "10.5.*"
},
"suggest": {
"ext-imagick": "Imagick extension is required for Imagick adapter"
},
"type": "library",
"autoload": {
@ -4368,9 +4370,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.8.4"
"source": "https://github.com/utopia-php/image/tree/0.8.5"
},
"time": "2025-06-03T08:32:20+00:00"
"time": "2026-04-17T15:02:49+00:00"
},
{
"name": "utopia-php/locale",
@ -4642,16 +4644,16 @@
},
{
"name": "utopia-php/platform",
"version": "0.12.1",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
"shasum": ""
},
"require": {
@ -4687,9 +4689,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
},
"time": "2026-04-08T04:11:31+00:00"
"time": "2026-04-17T09:57:18+00:00"
},
{
"name": "utopia-php/pools",
@ -5381,38 +5383,48 @@
},
{
"name": "webonyx/graphql-php",
"version": "v14.11.10",
"version": "v15.31.5",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8"
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.3",
"doctrine/coding-standard": "^6.0",
"nyholm/psr7": "^1.2",
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.94.2",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "0.12.82",
"phpstan/phpstan-phpunit": "0.12.18",
"phpstan/phpstan-strict-rules": "0.12.9",
"phpunit/phpunit": "^7.2 || ^8.5",
"psr/http-message": "^1.0",
"react/promise": "2.*",
"simpod/php-coveralls-mirror": "^3.0"
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.46",
"phpstan/phpstan-phpunit": "2.0.16",
"phpstan/phpstan-strict-rules": "2.0.10",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.0 || ^3.0",
"rector/rector": "^2.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
"ticketswap/phpstan-error-formatter": "1.3.0"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
@ -5434,15 +5446,19 @@
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
},
"funding": [
{
"url": "https://github.com/spawnia",
"type": "github"
},
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2023-07-05T14:23:37+00:00"
"time": "2026-04-11T18:06:15+00:00"
}
],
"packages-dev": [
@ -8449,5 +8465,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View file

@ -1286,6 +1286,7 @@ services:
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:

View file

@ -1 +0,0 @@
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.

View file

@ -1 +0,0 @@
Get a custom SMS template for the specified locale and type returning it's contents.

View file

@ -1 +0,0 @@
Update a custom SMS template for the specified locale and type. Use this endpoint to modify the content of your SMS templates.

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -7,6 +7,8 @@ use Appwrite\Event\Message\Execution as ExecutionMessage;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@ -30,9 +32,27 @@ class Log extends Listener
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
{
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
project: new Document($event->project),
execution: new Document($event->execution),
project: $project,
execution: $execution,
));
}
}

View file

@ -71,7 +71,9 @@ class Mails extends Listener
throw new \Exception('Invalid template path');
}
$customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? [];
$customTemplate =
$project->getAttribute('templates', [])["email.sessionAlert-" . $locale->default] ??
$project->getAttribute('templates', [])['email.sessionAlert-' . $locale->fallback] ?? [];
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Context;
use Utopia\Database\Document;
class Audit
{
public function __construct(
public ?Document $project = null,
public ?Document $user = null,
public string $mode = '',
public string $userAgent = '',
public string $ip = '',
public string $hostname = '',
public string $event = '',
public string $resource = '',
public array $payload = [],
) {
}
public function isEmpty(): bool
{
return $this->project === null
&& $this->user === null
&& $this->mode === ''
&& $this->userAgent === ''
&& $this->ip === ''
&& $this->hostname === ''
&& $this->event === ''
&& $this->resource === ''
&& $this->payload === [];
}
}

View file

@ -637,9 +637,11 @@ class Event
*/
$eventValues = \array_values($events);
/**
* Return a combined list of table, collection events and if tablesdb present then include all for backward compatibility
*/
$databaseType = $database?->getAttribute('type', 'legacy');
if ($database !== null && !\in_array($databaseType, ['legacy', 'tablesdb'], true)) {
return $eventValues;
}
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
}
@ -662,21 +664,30 @@ class Event
}
/**
* Adds `table` events for `collection` events.
* Adds table/collection counterpart events for backward compatibility.
*
* Example:
*
* `databases.*.collections.*.documents.*.update` →\
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
*
* `databases.*.tables.*.rows.*.update` →\
* `[databases.*.tables.*.rows.*.update, databases.*.collections.*.documents.*.update]`
*/
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
{
$tableEventMap = [
$collectionsToTablesMap = [
'documents' => 'rows',
'collections' => 'tables',
'attributes' => 'columns',
];
$tablesToCollectionsMap = [
'rows' => 'documents',
'tables' => 'collections',
'columns' => 'attributes',
];
$databasesEventMap = [
'tablesdb' => 'databases',
'tables' => 'collections',
@ -687,7 +698,10 @@ class Event
if (
(
str_contains($pattern, 'databases.') &&
str_contains($firstEvent, 'collections')
(
str_contains($firstEvent, 'collections') ||
str_contains($firstEvent, 'tables')
)
) ||
(
str_contains($firstEvent, 'tablesdb.')
@ -698,25 +712,16 @@ class Event
$pairedEvents[] = $event;
// tablesdb needs databases event with tables and collections
if (str_contains($event, 'tablesdb')) {
$databasesSideEvent = str_replace(
array_keys($databasesEventMap),
array_values($databasesEventMap),
$event
);
$databasesSideEvent = self::replaceEventSegments($event, $databasesEventMap);
$pairedEvents[] = $databasesSideEvent;
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$databasesSideEvent
);
$tableSideEvent = self::replaceEventSegments($databasesSideEvent, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'collections')) {
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$event
);
$tableSideEvent = self::replaceEventSegments($event, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'tables')) {
$collectionSideEvent = self::replaceEventSegments($event, $tablesToCollectionsMap);
$pairedEvents[] = $collectionSideEvent;
}
}
@ -728,6 +733,20 @@ class Event
return array_values(array_unique($events));
}
/**
* Replace only exact event path segments, never partial substrings.
*/
private static function replaceEventSegments(string $event, array $map): string
{
$parts = \explode('.', $event);
$parts = \array_map(
fn (string $part) => $map[$part] ?? $part,
$parts
);
return \implode('.', $parts);
}
/**
* Maps event terminology based on database type
*/

View file

@ -0,0 +1,71 @@
<?php
namespace Appwrite\Event\Message;
use Appwrite\Event\Context\Audit as AuditContext;
use Utopia\Database\Document;
final class Audit extends Base
{
public function __construct(
public readonly string $event,
public readonly array $payload,
public readonly Document $project = new Document(),
public readonly Document $user = new Document(),
public readonly string $resource = '',
public readonly string $mode = '',
public readonly string $ip = '',
public readonly string $userAgent = '',
public readonly string $hostname = '',
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'user' => $this->user->getArrayCopy(),
'payload' => $this->payload,
'resource' => $this->resource,
'mode' => $this->mode,
'ip' => $this->ip,
'userAgent' => $this->userAgent,
'event' => $this->event,
'hostname' => $this->hostname,
];
}
public static function fromArray(array $data): static
{
return new self(
event: $data['event'] ?? '',
payload: $data['payload'] ?? [],
project: new Document($data['project'] ?? []),
user: new Document($data['user'] ?? []),
resource: $data['resource'] ?? '',
mode: $data['mode'] ?? '',
ip: $data['ip'] ?? '',
userAgent: $data['userAgent'] ?? '',
hostname: $data['hostname'] ?? '',
);
}
public static function fromContext(AuditContext $context): static
{
return new self(
event: $context->event,
payload: $context->payload,
project: $context->project ?? new Document(),
user: $context->user ?? new Document(),
resource: $context->resource,
mode: $context->mode,
ip: $context->ip,
userAgent: $context->userAgent,
hostname: $context->hostname,
);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Certificate extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $domain,
public readonly bool $skipRenewCheck = false,
public readonly ?string $validationDomain = null,
public readonly string $action = \Appwrite\Event\Certificate::ACTION_GENERATION,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'domain' => $this->domain->getArrayCopy(),
'skipRenewCheck' => $this->skipRenewCheck,
'validationDomain' => $this->validationDomain,
'action' => $this->action,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
domain: new Document($data['domain'] ?? []),
skipRenewCheck: $data['skipRenewCheck'] ?? false,
validationDomain: $data['validationDomain'] ?? null,
action: $data['action'] ?? \Appwrite\Event\Certificate::ACTION_GENERATION,
);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Screenshot extends Base
{
public function __construct(
public readonly Document $project,
public readonly string $deploymentId,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'deploymentId' => $this->deploymentId,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
deploymentId: $data['deploymentId'] ?? '',
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Audit as AuditMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Audit extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(AuditMessage $message): string|bool
{
// Audit delivery is best-effort and should never fail the request lifecycle.
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[Audit] Failed to publish audit message: ' . $th->getMessage());
return false;
}
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Certificate as CertificateMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Certificate extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(CertificateMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Screenshot as ScreenshotMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Screenshot extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(ScreenshotMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}

View file

@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
/**
* Create a new promise that resolves when all passed in promises resolve.
*
* @param array $promisesOrValues
* @param iterable $promisesOrValues
* @return GQLPromise
*/
abstract public function all(array $promisesOrValues): GQLPromise;
abstract public function all(iterable $promisesOrValues): GQLPromise;
}

View file

@ -35,8 +35,12 @@ class Swoole extends Adapter
return new GQLPromise($promise, $this);
}
public function all(array $promisesOrValues): GQLPromise
public function all(iterable $promisesOrValues): GQLPromise
{
if ($promisesOrValues instanceof \Traversable) {
$promisesOrValues = \iterator_to_array($promisesOrValues);
}
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\GraphQL;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
final class ResolverLock
{
public Channel $channel;
public ?int $owner = null;
public int $depth = 0;
public function __construct()
{
$this->channel = new Channel(1);
}
/**
* Acquire the lock. Re-entering from the same coroutine only
* increments depth to avoid self-deadlock.
*/
public function acquire(): void
{
$cid = Coroutine::getCid();
if ($this->owner === $cid) {
$this->depth++;
return;
}
$this->channel->push(true);
$this->owner = $cid;
$this->depth = 1;
}
/**
* Release the lock.
*/
public function release(): void
{
if ($this->owner !== Coroutine::getCid()) {
return;
}
$this->depth--;
if ($this->depth > 0) {
return;
}
$this->owner = null;
$this->channel->pop();
}
}

View file

@ -6,6 +6,7 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@ -13,6 +14,75 @@ use Utopia\System\System;
class Resolvers
{
/**
* Request-scoped locks keyed by the per-request GraphQL Http instance.
*
* @var array<string, ResolverLock>
*/
private static array $locks = [];
/**
* Preserve response side effects that callers depend on, such as session
* cookies created by account auth routes.
*/
private static function mergeResponseSideEffects(Response $from, Response $to): void
{
foreach ($from->getCookies() as $cookie) {
$to->removeCookie($cookie['name']);
$to->addCookie(
$cookie['name'],
$cookie['value'],
$cookie['expire'],
$cookie['path'],
$cookie['domain'],
$cookie['secure'],
$cookie['httponly'],
$cookie['samesite']
);
}
$headers = $from->getHeaders();
$fallbackCookies = $headers['X-Fallback-Cookies'] ?? null;
if ($fallbackCookies === null) {
return;
}
$to->removeHeader('X-Fallback-Cookies');
foreach ((array) $fallbackCookies as $value) {
$to->addHeader('X-Fallback-Cookies', $value);
}
}
/**
* Get the current request container.
*/
private static function getResolverContainer(Http $utopia): Container
{
$container = $utopia->getResource('container');
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
/** @var Container $container */
return $container;
}
/** @var callable(): Container $container */
return $container();
}
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
*/
private static function getLock(Http $utopia): ResolverLock
{
$key = \spl_object_hash($utopia);
if (!isset(self::$locks[$key])) {
self::$locks[$key] = new ResolverLock();
}
return self::$locks[$key];
}
/**
* Create a resolver for a given API {@see Route}.
*
@ -24,34 +94,39 @@ class Resolvers
Http $utopia,
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($route, $args): void {
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
);
});
}
/**
@ -91,18 +166,23 @@ class Resolvers
string $collectionId,
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@ -122,23 +202,29 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
$beforeResolve = function ($payload) {
return $payload['documents'];
};
$beforeResolve = function ($payload) {
return $payload['documents'];
};
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
beforeResolve: $beforeResolve,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@ -158,19 +244,24 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@ -190,19 +281,24 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@ -220,18 +316,23 @@ class Resolvers
string $collectionId,
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@ -241,7 +342,7 @@ class Resolvers
* @param callable $resolve
* @param callable $reject
* @param callable|null $beforeResolve
* @param callable|null $beforeReject
* @param callable|null $prepareRequest
* @return void
* @throws Exception
*/
@ -252,39 +353,67 @@ class Resolvers
callable $resolve,
callable $reject,
?callable $beforeResolve = null,
?callable $beforeReject = null,
?callable $prepareRequest = null,
): void {
// Drop json content type so post args are used directly
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
$lock = self::getLock($utopia);
$request = clone $request;
$utopia->setResource('request', static fn () => $request);
$response->setContentType(Response::CONTENT_TYPE_NULL);
$response->clearSent();
$lock->acquire();
$original = $utopia->getRoute();
try {
$route = $utopia->match($request, fresh: true);
$request = clone $request;
$utopia->execute($route, $request, $response);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
// Drop json content type so post args are used directly.
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
if ($prepareRequest) {
$prepareRequest($request);
}
/** @var Response $resolverResponse */
$resolverResponse = clone $utopia->getResource('response');
$container = self::getResolverContainer($utopia);
$container->set('request', static fn () => $request);
$container->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
$route = $utopia->match($request, fresh: true);
$request->setRoute($route);
$utopia->execute($route, $request, $resolverResponse);
self::mergeResponseSideEffects($resolverResponse, $response);
if ($resolverResponse->isSent()) {
$response
->setStatusCode($resolverResponse->getStatusCode())
->setSent(true);
$resolve(null);
return;
}
$payload = $resolverResponse->getPayload();
$statusCode = $resolverResponse->getStatusCode();
} catch (\Throwable $e) {
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
$lock->release();
unset(self::$locks[\spl_object_hash($utopia)]);
}
$payload = $response->getPayload();
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
if ($beforeReject) {
$payload = $beforeReject($payload);
}
if ($statusCode < 200 || $statusCode >= 400) {
$reject(new GQLException(
message: $payload['message'],
code: $response->getStatusCode()
code: $statusCode
));
return;
}

View file

@ -3,12 +3,13 @@
namespace Appwrite\GraphQL\Types;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Assoc extends Json
{
public $name = 'Assoc';
public $description = 'The `Assoc` scalar type represents associative array values.';
public string $name = 'Assoc';
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
public function serialize($value)
{
@ -30,6 +31,10 @@ class Assoc extends Json
public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return \json_decode($valueNode->value, true);
if ($valueNode instanceof StringValueNode) {
return \json_decode($valueNode->value, true);
}
return parent::parseLiteral($valueNode, $variables);
}
}

View file

@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
class InputFile extends ScalarType
{
public $name = 'InputFile';
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
public string $name = 'InputFile';
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
public function serialize($value)

View file

@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Json extends ScalarType
{
public $name = 'Json';
public $description = 'The `JSON` scalar type represents JSON values as specified by
public string $name = 'Json';
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
public function serialize($value)

View file

@ -170,11 +170,6 @@ class Create extends Action
$message = Template::fromFile($templatesPath . '/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
$messageContent
->setParam('{{project}}', $projectName)
@ -223,7 +218,9 @@ class Create extends Action
$preview = $locale->getText("emails.mfaChallenge.preview");
$heading = $locale->getText("emails.mfaChallenge.heading");
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ??
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->fallback] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();

View file

@ -50,6 +50,11 @@ class Create extends Action
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return false;
}
public function __construct()
{
$this
@ -139,30 +144,42 @@ class Create extends Action
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
$hasData = !empty($data);
$hasDocuments = !empty($documents);
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// When empty documents are supported, an empty payload should still be treated as single create.
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
$data = [];
$hasData = true;
}
if ($hasData && $hasDocuments) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
}
if (!empty($data) && empty($documentId)) {
if ($hasData && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
if ($hasDocuments && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
@ -170,13 +187,13 @@ class Create extends Action
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
if ($hasDocuments && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
$isBulk = $hasDocuments;
if ($hasData) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;

View file

@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this

View file

@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listDocumentsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/documentsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listDocumentsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/documentsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listVectorsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/vectorsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}

View file

@ -1,57 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listVectorsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/vectorsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}

View file

@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
@ -69,7 +67,6 @@ class DocumentsDB extends Base
$service->addAction(UpdateTable::getName(), new UpdateTable());
$service->addAction(DeleteTable::getName(), new DeleteTable());
$service->addAction(ListTables::getName(), new ListTables());
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
}
@ -92,7 +89,6 @@ class DocumentsDB extends Base
$service->addAction(DeleteRow::getName(), new DeleteRow());
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}

View file

@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
@ -69,7 +67,6 @@ class VectorsDB extends Base
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
$service->addAction(ListCollections::getName(), new ListCollections());
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
}
@ -92,7 +89,6 @@ class VectorsDB extends Base
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
}
private function registerTransactionActions(Service $service): void

View file

@ -31,7 +31,7 @@ class Get extends Action
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', ['type' => 'output'])
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download')
->groups(['api', 'functions'])
->desc('Get deployment download')
->label('scope', 'functions.read')

View file

@ -6,9 +6,9 @@ use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
@ -58,7 +58,7 @@ class Builds extends Action
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
@ -84,7 +84,7 @@ class Builds extends Action
Document $project,
Database $dbForPlatform,
Event $queueForEvents,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@ -126,7 +126,7 @@ class Builds extends Action
$deviceForFunctions,
$deviceForSites,
$deviceForFiles,
$queueForScreenshots,
$publisherForScreenshots,
$queueForWebhooks,
$queueForFunctions,
$queueForRealtime,
@ -144,7 +144,8 @@ class Builds extends Action
$log,
$executor,
$plan,
$platform
$platform,
(int) ($payload['timeout'] ?? System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900))
);
break;
@ -161,7 +162,7 @@ class Builds extends Action
Device $deviceForFunctions,
Device $deviceForSites,
Device $deviceForFiles,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@ -179,7 +180,8 @@ class Builds extends Action
Log $log,
Executor $executor,
array $plan,
array $platform
array $platform,
int $timeout
): void {
Console::info('Deployment action started');
@ -592,10 +594,7 @@ class Builds extends Action
$cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT;
$memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, $minMemory);
$timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
$jwtExpiry = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $timeout, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
@ -1120,10 +1119,10 @@ class Builds extends Action
/** Screenshot site */
if ($resource->getCollection() === 'sites') {
$queueForScreenshots
->setDeploymentId($deployment->getId())
->setProject($project)
->trigger();
$publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot(
project: $project,
deploymentId: $deployment->getId(),
));
Console::log('Site screenshot queued');
}

View file

@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Message\Screenshot;
use Appwrite\Event\Realtime;
use Appwrite\Permission;
use Appwrite\Role;
@ -62,9 +63,11 @@ class Screenshots extends Action
throw new \Exception('Missing payload');
}
$screenshotMessage = Screenshot::fromArray($payload);
Console::log('Site screenshot started');
$deploymentId = $payload['deploymentId'] ?? null;
$deploymentId = $screenshotMessage->deploymentId;
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {

View file

@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits;
use Appwrite\Event\Audit;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$size = $publisherForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);

View file

@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Certificate $queueForCertificates, Response $response): void
public function action(int|string $threshold, Certificate $publisherForCertificates, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForCertificates->getSize();
$size = $publisherForCertificates->getSize();
$this->assertQueueThreshold($size, $threshold);

View file

@ -2,19 +2,19 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
@ -75,17 +75,17 @@ class Get extends Base
->inject('response')
->inject('queueForDatabase')
->inject('queueForDeletes')
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('queueForMails')
->inject('queueForFunctions')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('publisherForMigrations')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->callback($this->action(...));
}
@ -95,32 +95,32 @@ class Get extends Base
Response $response,
Database $queueForDatabase,
Delete $queueForDeletes,
Audit $queueForAudits,
Audit $publisherForAudits,
Mail $queueForMails,
Func $queueForFunctions,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Build $queueForBuilds,
Messaging $queueForMessaging,
MigrationPublisher $publisherForMigrations,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
): void {
$threshold = (int) $threshold;
$queue = match ($name) {
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates,
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
};

View file

@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs;
use Appwrite\Event\Audit;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$size = $publisherForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);

View file

@ -1,7 +1,8 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@ -21,27 +22,28 @@ class Update extends Action
public static function getName()
{
return 'updateProjectProtocolStatus';
return 'updateProjectProtocol';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/protocols/:protocolId/status')
->setHttpPath('/v1/project/protocols/:protocolId')
->httpAlias('/v1/project/protocols/:protocolId/status')
->httpAlias('/v1/projects/:projectId/api')
->desc('Update project protocol status')
->desc('Update project protocol')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'protocols.[protocol].update')
->label('audits.event', 'project.protocols.[protocol].update')
->label('event', 'protocols.[protocolId].update')
->label('audits.event', 'project.protocols.[protocolId].update')
->label('audits.resource', 'project.protocols/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateProtocolStatus',
name: 'updateProtocol',
description: <<<EOT
Update the status of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
Update properties of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
@ -57,6 +59,7 @@ class Update extends Action
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
@ -66,7 +69,8 @@ class Update extends Action
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
Authorization $authorization,
Event $queueForEvents,
): void {
$protocols = $project->getAttribute('apis', []);
$protocols[$protocolId] = $enabled;
@ -75,6 +79,8 @@ class Update extends Action
'apis' => $protocols,
])));
$queueForEvents->setParam('protocolId', $protocolId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}

View file

@ -1,7 +1,8 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
namespace Appwrite\Platform\Modules\Project\Http\Project\Services;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@ -21,27 +22,28 @@ class Update extends Action
public static function getName()
{
return 'updateProjectServiceStatus';
return 'updateProjectService';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/services/:serviceId/status')
->setHttpPath('/v1/project/services/:serviceId')
->httpAlias('/v1/project/services/:serviceId/status')
->httpAlias('/v1/projects/:projectId/service')
->desc('Update project service status')
->desc('Update project service')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'services.[service].update')
->label('audits.event', 'project.services.[service].update')
->label('event', 'services.[serviceId].update')
->label('audits.event', 'project.services.[serviceId].update')
->label('audits.resource', 'project.services/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateServiceStatus',
name: 'updateService',
description: <<<EOT
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
Update properties of a specific service. Use this endpoint to enable or disable a service in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
@ -57,6 +59,7 @@ class Update extends Action
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
@ -66,7 +69,8 @@ class Update extends Action
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
Authorization $authorization,
Event $queueForEvents
): void {
$services = $project->getAttribute('services', []);
$services[$serviceId] = $enabled;
@ -75,6 +79,8 @@ class Update extends Action
'services' => $services,
])));
$queueForEvents->setParam('serviceId', $serviceId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}

View file

@ -22,8 +22,8 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status\Update as UpdateProjectProtocolStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Services\Status\Update as UpdateProjectServiceStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Update as UpdateProjectProtocol;
use Appwrite\Platform\Modules\Project\Http\Project\Services\Update as UpdateProjectService;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@ -42,8 +42,8 @@ class Http extends Service
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectProtocolStatus::getName(), new UpdateProjectProtocolStatus());
$this->addAction(UpdateProjectServiceStatus::getName(), new UpdateProjectServiceStatus());
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@ -62,7 +62,7 @@ class Create extends Action
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('platform')
@ -70,7 +70,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@ -114,13 +114,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@ -66,7 +66,7 @@ class Create extends Action
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@ -75,7 +75,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@ -132,13 +132,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@ -69,7 +69,7 @@ class Create extends Action
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@ -78,7 +78,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@ -136,13 +136,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@ -66,7 +66,7 @@ class Create extends Action
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@ -75,7 +75,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@ -132,13 +132,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@ -56,7 +56,7 @@ class Update extends Action
))
->param('ruleId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Rule ID.', false, ['dbForProject'])
->inject('response')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
@ -67,7 +67,7 @@ class Update extends Action
public function action(
string $ruleId,
Response $response,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Event $queueForEvents,
Document $project,
Database $dbForPlatform,
@ -110,12 +110,13 @@ class Update extends Action
}
// Issue a TLS certificate when DNS verification is successful
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
]),
));
if (!empty($certificate)) {
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));

View file

@ -54,7 +54,7 @@ class Get extends Action
->label('cache', true)
->label('cache.resourceType', 'bucket/{request.bucketId}')
->label('cache.resource', 'file/{request.fileId}')
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output'])
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output', 'project'])
->label('sdk', new Method(
namespace: 'storage',
group: 'files',

View file

@ -324,7 +324,9 @@ class Create extends Action
$body = $locale->getText('emails.invitation.body');
$preview = $locale->getText('emails.invitation.preview');
$subject = $locale->getText('emails.invitation.subject');
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.invitation-' . $locale->default] ??
$project->getAttribute('templates', [])['email.invitation-' . $locale->fallback] ?? [];
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-inner-base.tpl');
$message
@ -407,11 +409,6 @@ class Create extends Action
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
if (! empty($customTemplate)) {
$message = $customTemplate['message'];
}
$message = $message->setParam('{{token}}', $url);
$message = $message->render();

View file

@ -126,6 +126,7 @@ class Delete extends Action
if ($team->getAttribute('userInternalId') === $membership->getAttribute('userInternalId')) {
$membership = $dbForProject->findOne('memberships', [
Query::equal('teamInternalId', [$team->getSequence()]),
Query::equal('confirm', [true]),
]);
if (!$membership->isEmpty()) {

View file

@ -307,6 +307,7 @@ class Create extends Action
];
}
$output->setAttribute('type', $type);
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);

View file

@ -313,6 +313,7 @@ class XList extends Action
}, $repos);
$response->dynamic(new Document([
'type' => $type,
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);

View file

@ -2,7 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use DateTime;
use Swoole\Coroutine\Channel;
use Swoole\Process;
@ -29,16 +29,16 @@ class Interval extends Action
->desc('Schedules tasks on regular intervals by publishing them to our queues')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->callback($this->action(...));
}
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): void
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): void
{
Console::title('Interval V1');
Console::success(APP_NAME . ' interval process v1 has started');
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $queueForCertificates);
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $publisherForCertificates);
$chan = new Channel(1);
Process::signal(SIGTERM, function () use ($chan) {
@ -52,16 +52,16 @@ class Interval extends Action
}
}
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): array
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): array
{
$timers = [];
$tasks = $this->getTasks();
foreach ($tasks as $task) {
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $queueForCertificates) {
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $publisherForCertificates) {
$taskName = $task['name'];
Span::init("interval.{$taskName}");
try {
$task['callback']($dbForPlatform, $getProjectDB, $queueForCertificates);
$task['callback']($dbForPlatform, $getProjectDB, $publisherForCertificates);
} catch (\Exception $e) {
Span::error($e);
} finally {
@ -80,15 +80,15 @@ class Interval extends Action
return [
[
'name' => 'domainVerification',
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates) {
$this->verifyDomain($dbForPlatform, $queueForCertificates);
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates) {
$this->verifyDomain($dbForPlatform, $publisherForCertificates);
},
'interval' => $intervalDomainVerification * 1000,
]
];
}
private function verifyDomain(Database $dbForPlatform, Certificate $queueForCertificates): void
private function verifyDomain(Database $dbForPlatform, Certificate $publisherForCertificates): void
{
$time = DatabaseDateTime::now();
$fromTime = new DateTime('-3 days'); // Max 3 days old
@ -115,13 +115,17 @@ class Interval extends Action
foreach ($rules as $rule) {
try {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_DOMAIN_VERIFICATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION,
));
$processed++;
} catch (\Throwable $th) {
$failed++;

View file

@ -2,8 +2,8 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Publisher\Certificate;
use DateInterval;
use DateTime;
use Utopia\Console;
@ -29,12 +29,12 @@ class Maintenance extends Action
->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.')
->inject('dbForPlatform')
->inject('console')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForDeletes')
->callback($this->action(...));
}
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@ -59,7 +59,7 @@ class Maintenance extends Action
$delay = $next->getTimestamp() - $now->getTimestamp();
}
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) {
$time = DatabaseDateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
@ -92,7 +92,7 @@ class Maintenance extends Action
->trigger();
$this->notifyDeleteConnections($queueForDeletes);
$this->renewCertificates($dbForPlatform, $queueForCertificates);
$this->renewCertificates($dbForPlatform, $publisherForCertificates);
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
$this->notifyDeleteCSVExports($queueForDeletes);
@ -124,7 +124,7 @@ class Maintenance extends Action
->trigger();
}
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void
{
$time = DatabaseDateTime::now();
@ -158,13 +158,17 @@ class Maintenance extends Action
continue;
}
$queueForCertificate
->setDomain(new Document([
$publisherForCertificate->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
}

View file

@ -622,29 +622,28 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$repo->execute('config', 'advice.defaultBranchName', 'false');
$repo->addRemote('origin', $gitUrl);
// Fetch and checkout base branch (or create if new repo)
// Fetch and checkout the target branch (e.g. dev) if it exists on remote,
// otherwise create it from the base branch (e.g. main).
// We build on top of the existing remote branch so a regular push
// works without force-pushing against protected branches.
$hasBranch = false;
try {
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $gitBranch);
$hasBranch = true;
} catch (\Throwable) {
// Branch doesn't exist on remote yet
}
if ($hasBranch) {
$repo->execute('checkout', '-f', $gitBranch);
} else {
// Fetch base branch to create the target branch from it
try {
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
$repo->execute('checkout', '-f', $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
try {
$repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags');
} catch (\Throwable) {
}
// Create or checkout dev branch from the base branch
// This ensures dev always starts from the latest base branch,
// avoiding history divergence caused by squash merges.
try {
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $gitBranch);
}
@ -685,7 +684,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return true;
}
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable $e) {
Console::warning(" Git push failed: " . $e->getMessage());
return false;

View file

@ -2,7 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -29,11 +29,11 @@ class SSL extends Action
->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->callback($this->action(...));
}
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates): void
{
$domain = new Domain(!empty($domain) ? $domain : '');
if (!$domain->isKnown() || $domain->isTest()) {
@ -98,12 +98,13 @@ class SSL extends Action
Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get());
}
$queueForCertificates
->setDomain(new Document([
'domain' => $domain->get()
]))
->setSkipRenewCheck($skipCheck)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: new Document([
'domain' => $domain->get(),
]),
skipRenewCheck: $skipCheck,
));
Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get());
}

View file

@ -7,6 +7,8 @@ use Cron\CronExpression;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Span\Span;
use Utopia\System\System;
/**
* ScheduleFunctions
@ -88,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
return;
continue;
}
$schedule = $this->schedules[$scheduleKey];
@ -102,8 +104,22 @@ class ScheduleFunctions extends ScheduleBase
->setFunction($schedule['resource'])
->setMethod('POST')
->setPath('/')
->setProject($schedule['project'])
->trigger();
->setProject($schedule['project']);
$projectDoc = $schedule['project'];
$functionDoc = $schedule['resource'];
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
Span::init('execution.trace.v1_functions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $projectDoc->getId());
Span::add('functionId', $functionDoc->getId());
Span::add('scheduleId', $schedule['$id'] ?? '');
Span::current()?->finish();
}
$queueForFunctions->trigger();
$this->recordEnqueueDelay($delayConfig['nextDate']);
}

View file

@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Message\Audit;
use Exception;
use Throwable;
use Utopia\Console;
@ -40,7 +41,6 @@ class Audits extends Action
$this
->desc('Audits worker')
->inject('message')
->inject('project')
->inject('getAudit')
->callback($this->action(...));
@ -50,14 +50,13 @@ class Audits extends Action
/**
* @param Message $message
* @param Document $project
* @param callable(Document): \Utopia\Audit\Audit $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Structure
*/
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
public function action(Message $message, callable $getAudit): Commit|NoCommit
{
$payload = $message->getPayload() ?? [];
@ -65,19 +64,21 @@ class Audits extends Action
throw new Exception('Missing payload');
}
$auditMessage = Audit::fromArray($payload);
Console::info('Aggregating audit logs');
$event = $payload['event'] ?? '';
$event = $auditMessage->event;
$auditPayload = '';
if ($project->getId() === 'console') {
$auditPayload = $payload['payload'] ?? '';
if ($auditMessage->project->getId() === 'console') {
$auditPayload = $auditMessage->payload;
}
$mode = $payload['mode'] ?? '';
$resource = $payload['resource'] ?? '';
$userAgent = $payload['userAgent'] ?? '';
$ip = $payload['ip'] ?? '';
$user = new Document($payload['user'] ?? []);
$mode = $auditMessage->mode;
$resource = $auditMessage->resource;
$userAgent = $auditMessage->userAgent;
$ip = $auditMessage->ip;
$user = $auditMessage->user;
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
$actorUserId = $impersonatorUserId ?: $user->getId();
@ -126,14 +127,14 @@ class Audits extends Action
];
}
if (isset($this->logs[$project->getSequence()])) {
$this->logs[$project->getSequence()]['logs'][] = $eventData;
if (isset($this->logs[$auditMessage->project->getSequence()])) {
$this->logs[$auditMessage->project->getSequence()]['logs'][] = $eventData;
} else {
$this->logs[$project->getSequence()] = [
$this->logs[$auditMessage->project->getSequence()] = [
'project' => new Document([
'$id' => $project->getId(),
'$sequence' => $project->getSequence(),
'database' => $project->getAttribute('database'),
'$id' => $auditMessage->project->getId(),
'$sequence' => $auditMessage->project->getSequence(),
'database' => $auditMessage->project->getAttribute('database'),
]),
'logs' => [$eventData]
];

View file

@ -3,10 +3,10 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
@ -55,7 +55,7 @@ class Certificates extends Action
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('log')
->inject('certificates')
->inject('plan')
@ -71,7 +71,7 @@ class Certificates extends Action
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $queueForCertificates
* @param Certificate $publisherForCertificates
* @param Log $log
* @param CertificatesAdapter $certificates
* @param array $plan
@ -88,7 +88,7 @@ class Certificates extends Action
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Log $log,
CertificatesAdapter $certificates,
array $plan,
@ -100,21 +100,22 @@ class Certificates extends Action
throw new Exception('Missing payload');
}
$document = new Document($payload['domain'] ?? []);
$certificateMessage = \Appwrite\Event\Message\Certificate::fromArray($payload);
$document = $certificateMessage->domain;
$domain = new Domain($document->getAttribute('domain', ''));
$domainType = $document->getAttribute('domainType');
$skipRenewCheck = $payload['skipRenewCheck'] ?? false;
$validationDomain = $payload['validationDomain'] ?? null;
$action = $payload['action'] ?? Certificate::ACTION_GENERATION;
$skipRenewCheck = $certificateMessage->skipRenewCheck;
$validationDomain = $certificateMessage->validationDomain;
$action = $certificateMessage->action;
$log->addTag('domain', $domain->get());
switch ($action) {
case Certificate::ACTION_DOMAIN_VERIFICATION:
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $authorization, $validationDomain);
case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION:
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
break;
case Certificate::ACTION_GENERATION:
case \Appwrite\Event\Certificate::ACTION_GENERATION:
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
break;
@ -130,7 +131,7 @@ class Certificates extends Action
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $queueForCertificates
* @param Certificate $publisherForCertificates
* @param Log $log
* @param ValidatorAuthorization $authorization
* @param string|null $validationDomain
@ -146,7 +147,7 @@ class Certificates extends Action
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Log $log,
ValidatorAuthorization $authorization,
?string $validationDomain = null
@ -188,13 +189,17 @@ class Certificates extends Action
// Issue a TLS certificate when domain is verified
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
Console::success('Certificate generation triggered successfully.');
}

View file

@ -7,6 +7,8 @@ use Exception;
use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Executions extends Action
{
@ -39,6 +41,20 @@ class Executions extends Action
throw new Exception('Missing execution');
}
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.executions_worker_upsert');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $executionMessage->project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('resourceType', $execution->getAttribute('resourceType', ''));
Span::current()?->finish();
}
$dbForProject->upsertDocument('executions', $execution);
}
}

View file

@ -23,6 +23,7 @@ use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Functions extends Action
@ -115,6 +116,22 @@ class Functions extends Action
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_dequeue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('payloadType', $type);
Span::add('queuePid', $message->getPid());
Span::add('queueName', $message->getQueue());
Span::add('messageTimestamp', (string) $message->getTimestamp());
Span::current()?->finish();
}
}
if (!empty($events)) {
$limit = 100;
$sum = 100;
@ -304,6 +321,20 @@ class Functions extends Action
'duration' => 0.0,
]);
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('trigger', $trigger);
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@ -522,6 +553,18 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_executor');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $executionId);
Span::add('deploymentId', $deployment->getId());
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
@ -594,6 +637,19 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),

View file

@ -19,8 +19,7 @@ abstract class Promise
return;
}
$resolve = function ($value) {
$this->setResult($value);
$this->setState(self::STATE_FULFILLED);
$this->setState($this->setResult($value));
};
$reject = function ($value) {
$this->setResult($value);
@ -106,6 +105,11 @@ abstract class Promise
}
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
if (!\is_callable($callable)) {
if ($this->isRejected()) {
$reject($this->result);
return;
}
$resolve($this->result);
return;
}
@ -126,30 +130,36 @@ abstract class Promise
abstract public static function all(iterable $promises): self;
/**
* Set resolved result
* Set the resolved result, adopting nested promises while preserving
* whether the adopted promise fulfilled or rejected.
*
* @param mixed $value
* @return void
* @return int
*/
protected function setResult(mixed $value): void
protected function setResult(mixed $value): int
{
if (!\is_callable([$value, 'then'])) {
$this->result = $value;
return;
return self::STATE_FULFILLED;
}
$resolved = false;
$state = self::STATE_PENDING;
$callable = function ($value) use (&$resolved) {
$this->setResult($value);
$resolved = true;
};
$value->then(
function ($value) use (&$state) {
$state = $this->setResult($value);
},
function ($value) use (&$state) {
$this->result = $value;
$state = self::STATE_REJECTED;
}
);
$value->then($callable, $callable);
while (!$resolved) {
while ($state === self::STATE_PENDING) {
usleep(25000);
}
return $state;
}
/**

View file

@ -263,6 +263,182 @@ abstract class Format
return $contents;
}
/**
* @param array<Model> $models
* @return array<string, mixed>|null
*/
protected function getDiscriminator(array $models, string $refPrefix): ?array
{
if (\count($models) < 2) {
return null;
}
$candidateKeys = \array_keys($models[0]->conditions);
foreach (\array_slice($models, 1) as $model) {
$candidateKeys = \array_values(\array_intersect($candidateKeys, \array_keys($model->conditions)));
}
if (empty($candidateKeys)) {
return null;
}
foreach ($candidateKeys as $key) {
$mapping = [];
$isValid = true;
foreach ($models as $model) {
$rules = $model->getRules();
$condition = $model->conditions[$key] ?? null;
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
$isValid = false;
break;
}
if (!\is_array($condition)) {
if (!\is_scalar($condition)) {
$isValid = false;
break;
}
$values = [$condition];
} else {
if ($condition === []) {
$isValid = false;
break;
}
$values = $condition;
$hasInvalidValue = false;
foreach ($values as $value) {
if (!\is_scalar($value)) {
$hasInvalidValue = true;
break;
}
}
if ($hasInvalidValue) {
$isValid = false;
break;
}
}
if (isset($rules[$key]['enum']) && \is_array($rules[$key]['enum'])) {
$values = \array_values(\array_filter(
$values,
fn (mixed $value) => \in_array($value, $rules[$key]['enum'], true)
));
}
if ($values === []) {
$isValid = false;
break;
}
$ref = $refPrefix . $model->getType();
foreach ($values as $value) {
$mappingKey = \is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
if (isset($mapping[$mappingKey]) && $mapping[$mappingKey] !== $ref) {
$isValid = false;
break;
}
$mapping[$mappingKey] = $ref;
}
if (!$isValid) {
break;
}
}
if (!$isValid || $mapping === []) {
continue;
}
return [
'propertyName' => $key,
'mapping' => $mapping,
];
}
// Single-key failed — try compound discriminator
return $this->getCompoundDiscriminator($models, $refPrefix);
}
/**
* @param array<Model> $models
* @return array<string, mixed>|null
*/
private function getCompoundDiscriminator(array $models, string $refPrefix): ?array
{
$allKeys = [];
foreach ($models as $model) {
foreach (\array_keys($model->conditions) as $key) {
if (!\in_array($key, $allKeys, true)) {
$allKeys[] = $key;
}
}
}
if (\count($allKeys) < 2) {
return null;
}
$primaryKey = $allKeys[0];
$primaryMapping = [];
$compoundMapping = [];
foreach ($models as $model) {
$rules = $model->getRules();
$conditions = [];
foreach ($model->conditions as $key => $condition) {
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
return null;
}
if (!\is_scalar($condition)) {
return null;
}
$conditions[$key] = \is_bool($condition) ? ($condition ? 'true' : 'false') : (string) $condition;
}
if (empty($conditions)) {
return null;
}
$ref = $refPrefix . $model->getType();
$compoundMapping[$ref] = $conditions;
// Best-effort single-key mapping — last model with this value wins (fallback)
if (isset($conditions[$primaryKey])) {
$primaryMapping[$conditions[$primaryKey]] = $ref;
}
}
// Verify compound uniqueness
$seen = [];
foreach ($compoundMapping as $conditions) {
$sig = \json_encode($conditions, JSON_THROW_ON_ERROR);
if (isset($seen[$sig])) {
return null;
}
$seen[$sig] = true;
}
return \array_filter([
'propertyName' => $primaryKey,
'mapping' => !empty($primaryMapping) ? $primaryMapping : null,
'x-propertyNames' => $allKeys,
'x-mapping' => $compoundMapping,
]);
}
protected function getRequestEnumName(string $service, string $method, string $param): ?string
{
/* `$service` is `$namespace` */
@ -595,16 +771,6 @@ abstract class Format
return 'EmailTemplateLocale';
}
break;
case 'getSmsTemplate':
case 'updateSmsTemplate':
case 'deleteSmsTemplate':
switch ($param) {
case 'type':
return 'SmsTemplateType';
case 'locale':
return 'SmsTemplateLocale';
}
break;
case 'createPlatform':
switch ($param) {
case 'type':

View file

@ -316,9 +316,10 @@ class OpenAPI3 extends Format
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model)
],
'schema' => \array_filter([
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model),
'discriminator' => $this->getDiscriminator($model, '#/components/schemas/'),
]),
],
],
];
@ -900,18 +901,30 @@ class OpenAPI3 extends Format
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
if (\is_array($rule['type'])) {
$resolvedModels = \array_map(function (string $type) {
foreach ($this->models as $model) {
if ($model->getType() === $type) {
return $model;
}
}
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
}, $rule['type']);
if ($rule['array']) {
$items = [
$items = \array_filter([
'anyOf' => \array_map(function ($type) {
return ['$ref' => '#/components/schemas/' . $type];
}, $rule['type'])
];
}, $rule['type']),
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
]);
} else {
$items = [
$items = \array_filter([
'oneOf' => \array_map(function ($type) {
return ['$ref' => '#/components/schemas/' . $type];
}, $rule['type'])
];
}, $rule['type']),
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
]);
}
} else {
$items = [

View file

@ -322,11 +322,12 @@ class Swagger2 extends Format
}
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => $modelDescription,
'schema' => [
'schema' => \array_filter([
'x-oneOf' => \array_map(function ($m) {
return ['$ref' => '#/definitions/' . $m->getType()];
}, $model)
],
}, $model),
'x-discriminator' => $this->getDiscriminator($model, '#/definitions/'),
]),
];
} else {
// Response definition using one type
@ -880,14 +881,27 @@ class Swagger2 extends Format
$rule['type'] = ($rule['type']) ?: 'none';
if (\is_array($rule['type'])) {
$resolvedModels = \array_map(function (string $type) {
foreach ($this->models as $model) {
if ($model->getType() === $type) {
return $model;
}
}
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
}, $rule['type']);
$xDiscriminator = $this->getDiscriminator($resolvedModels, '#/definitions/');
if ($rule['array']) {
$items = [
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
];
$items = \array_filter([
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
'x-discriminator' => $xDiscriminator,
]);
} else {
$items = [
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
];
$items = \array_filter([
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
'x-discriminator' => $xDiscriminator,
]);
}
} else {
$items = [

View file

@ -238,6 +238,9 @@ class Request extends UtopiaRequest
if ($allowedParams !== null) {
$params = array_intersect_key($params, array_flip($allowedParams));
}
if (!isset($params['project'])) {
$params['project'] = $this->getHeader('x-appwrite-project', '');
}
ksort($params);
return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER);
}

View file

@ -35,6 +35,13 @@ class V19 extends Filter
case 'functions.updateVariable':
$content['secret'] = false;
break;
case 'functions.getDeploymentDownload':
// Pre-1.7.0 clients call the legacy alias
// `/v1/functions/:functionId/deployments/:deploymentId/build/download`,
// which always downloaded the build output. The merged 1.7.0 endpoint
// requires an explicit `type` param, so force it to `output` here.
$content['type'] = 'output';
break;
}
return $content;
}

View file

@ -73,10 +73,10 @@ class V22 extends Filter
public function parse(array $content, string $model): array
{
switch ($model) {
case 'project.updateServiceStatus':
case 'project.updateService':
$content = $this->parseUpdateServiceStatus($content);
break;
case 'project.updateProtocolStatus':
case 'project.updateProtocol':
$content = $this->parseUpdateProtocolStatus($content);
break;
case 'project.createKey':

View file

@ -265,7 +265,6 @@ class Response extends SwooleResponse
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
public const MODEL_VCS = 'vcs';
public const MODEL_SMS_TEMPLATE = 'smsTemplate';
public const MODEL_EMAIL_TEMPLATE = 'emailTemplate';
// Health
@ -613,6 +612,8 @@ class Response extends SwooleResponse
throw new \Exception('Response body is not a valid JSON object.');
}
$this->payload = \is_array($data) ? $data : (array) $data;
$this
->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8)
->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
@ -627,13 +628,12 @@ class Response extends SwooleResponse
}
/**
* Reset the sent flag so the response can be reused for another
* action execution (e.g. batched GraphQL queries that share one
* Response instance).
* Set the sent flag on the response. Pass false to allow reuse
* (e.g. batched GraphQL queries), true to prevent further writes.
*/
public function clearSent(): static
public function setSent(bool $sent): static
{
$this->sent = false;
$this->sent = $sent;
return $this;
}

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoArgon2 extends Model
{
public array $conditions = [
'type' => 'argon2',
];
public function __construct()
{
// No options if imported. If hashed by Appwrite, following configuration is available:

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoBcrypt extends Model
{
public array $conditions = [
'type' => 'bcrypt',
];
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoMd5 extends Model
{
public array $conditions = [
'type' => 'md5',
];
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoPhpass extends Model
{
public array $conditions = [
'type' => 'phpass',
];
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoScrypt extends Model
{
public array $conditions = [
'type' => 'scrypt',
];
public function __construct()
{
$this

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoScryptModified extends Model
{
public array $conditions = [
'type' => 'scryptMod',
];
public function __construct()
{
$this

View file

@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
class AlgoSha extends Model
{
public array $conditions = [
'type' => 'sha',
];
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration

View file

@ -7,9 +7,16 @@ use Appwrite\Utopia\Response\Model;
abstract class Detection extends Model
{
public function __construct()
public function __construct(string $type)
{
$this
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Repository detection type.',
'default' => $type,
'example' => $type,
'enum' => [$type],
])
->addRule('variables', [
'type' => Response::MODEL_DETECTION_VARIABLE,
'description' => 'Environment variables found in .env files',

View file

@ -8,7 +8,11 @@ class DetectionFramework extends Detection
{
public function __construct()
{
parent::__construct();
$this->conditions = [
'type' => 'framework',
];
parent::__construct('framework');
$this
->addRule('framework', [

View file

@ -8,7 +8,11 @@ class DetectionRuntime extends Detection
{
public function __construct()
{
parent::__construct();
$this->conditions = [
'type' => 'runtime',
];
parent::__construct('runtime');
$this
->addRule('runtime', [

View file

@ -0,0 +1,29 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryFrameworkList extends BaseList
{
public array $conditions = [
'type' => 'framework',
];
public function __construct()
{
parent::__construct(
'Framework Provider Repositories List',
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
'frameworkProviderRepositories',
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK
);
$this->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Provider repository list type.',
'default' => 'framework',
'example' => 'framework',
]);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryRuntimeList extends BaseList
{
public array $conditions = [
'type' => 'runtime',
];
public function __construct()
{
parent::__construct(
'Runtime Provider Repositories List',
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
'runtimeProviderRepositories',
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME
);
$this->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Provider repository list type.',
'default' => 'runtime',
'example' => 'runtime',
]);
}
}

Some files were not shown because too many files have changed in this diff Show more