Merge remote-tracking branch 'origin/1.9.x' into presence-api

This commit is contained in:
ArnabChatterjee20k
2026-04-16 12:44:29 +05:30
71 changed files with 1685 additions and 894 deletions
+1 -1
View File
@@ -512,7 +512,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
+5 -4
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()));
+4 -4
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' => ''
+12 -5
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.
}
}
+1 -1
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') {
+9 -8
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 {
+27 -28
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())) {
+1 -2
View File
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
@@ -533,6 +531,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
+15
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))
+2 -12
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);
-15
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']);
+1 -1
View File
@@ -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.*"
},
Generated
+40 -26
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": "f6a87c1012b316e614258f8f57a28e48",
"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",
@@ -5381,38 +5381,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 +5444,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 +8463,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+1
View File
@@ -1288,6 +1288,7 @@ services:
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:
+22 -2
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,
));
}
}
+34
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 === [];
}
}
+40 -21
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
*/
+71
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,
);
}
}
@@ -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,
);
}
}
+34
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'] ?? '',
);
}
}
+35
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);
}
}
@@ -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);
}
}
@@ -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);
}
}
+2 -2
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;
}
@@ -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);
}
}
+55
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();
}
}
+237 -108
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;
}
+8 -3
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);
}
}
+2 -2
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)
+2 -2
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)
@@ -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;
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this
@@ -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(...));
}
}
@@ -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(...));
}
}
@@ -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(...));
}
}
@@ -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(...));
}
}
@@ -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());
}
@@ -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
@@ -6,9 +6,9 @@ use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
@@ -58,7 +58,7 @@ class Builds extends Action
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
@@ -84,7 +84,7 @@ class Builds extends Action
Document $project,
Database $dbForPlatform,
Event $queueForEvents,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -126,7 +126,7 @@ class Builds extends Action
$deviceForFunctions,
$deviceForSites,
$deviceForFiles,
$queueForScreenshots,
$publisherForScreenshots,
$queueForWebhooks,
$queueForFunctions,
$queueForRealtime,
@@ -161,7 +161,7 @@ class Builds extends Action
Device $deviceForFunctions,
Device $deviceForSites,
Device $deviceForFiles,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -1120,10 +1120,10 @@ class Builds extends Action
/** Screenshot site */
if ($resource->getCollection() === 'sites') {
$queueForScreenshots
->setDeploymentId($deployment->getId())
->setProject($project)
->trigger();
$publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot(
project: $project,
deploymentId: $deployment->getId(),
));
Console::log('Site screenshot queued');
}
@@ -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()) {
@@ -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);
@@ -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);
@@ -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,
};
@@ -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);
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -33,8 +34,8 @@ class Update extends Action
->desc('Update project protocol status')
->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',
@@ -57,6 +58,7 @@ class Update extends Action
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
@@ -66,7 +68,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 +78,8 @@ class Update extends Action
'apis' => $protocols,
])));
$queueForEvents->setParam('protocolId', $protocolId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -33,8 +34,8 @@ class Update extends Action
->desc('Update project service status')
->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',
@@ -57,6 +58,7 @@ class Update extends Action
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
@@ -66,7 +68,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 +78,8 @@ class Update extends Action
'services' => $services,
])));
$queueForEvents->setParam('serviceId', $serviceId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -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());
@@ -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());
@@ -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());
@@ -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());
@@ -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', ''));
@@ -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()) {
+19 -15
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++;
+15 -11
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,
));
}
}
+17 -18
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;
+10 -9
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());
}
@@ -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']);
}
+18 -17
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]
];
+23 -18
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.');
}
+11 -3
View File
@@ -760,11 +760,19 @@ class Deletes extends Action
$databasesToClean
));
} elseif ($sharedTablesV1) {
/**
* Temporary disabling deletes for internal collections
*/
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$queries[] = Query::orderAsc();
$this->deleteByGroup(
Database::METADATA,
[
Query::orderAsc()
],
$queries,
$dbForProject
);
} elseif ($sharedTablesV2) {
@@ -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);
}
}
@@ -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(),
+24 -14
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;
}
/**
+6 -5
View File
@@ -615,6 +615,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));
@@ -629,13 +631,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;
}
+1 -1
View File
@@ -22,7 +22,7 @@ class Comment
'Every Git commit and branch gets its own deployment URL automatically',
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
'HTTPS and SSL certificates are handled automatically for all your Sites',
'Functions can run for up to 15 minutes before timing out',
'Function builds can take up to 45 minutes before timing out',
'Schedule functions to run as often as every minute with cron expressions',
'Environment variables can be scoped per function or shared across your project',
'Function scopes give you fine-grained control over API permissions',
@@ -14,7 +14,12 @@ class AccountConsoleClientTest extends Scope
use ProjectConsole;
use SideClient;
public function testDeleteAccount(): void
/**
* Test that account deletion succeeds even with active team memberships.
* When the user is the sole owner and only member of a team, the team
* should be cleaned up automatically.
*/
public function testDeleteAccountWithMembership(): void
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
@@ -46,7 +51,7 @@ class AccountConsoleClientTest extends Scope
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
// create team
// Create team — user becomes sole owner and only member
$team = $this->client->call(Client::METHOD_POST, '/teams', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
@@ -58,7 +63,51 @@ class AccountConsoleClientTest extends Scope
]);
$this->assertEquals($team['headers']['status-code'], 201);
$teamId = $team['body']['$id'];
// Account deletion should succeed even with active membership
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(204, $response['headers']['status-code']);
}
/**
* Test that account deletion works when the user has no team memberships.
*/
public function testDeleteAccountWithoutMembership(): void
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
'origin' => 'http://localhost',
@@ -67,27 +116,7 @@ class AccountConsoleClientTest extends Scope
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals($response['headers']['status-code'], 400);
// DELETE TEAM
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals($response['headers']['status-code'], 204);
$this->assertEventually(function () use ($session) {
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(204, $response['headers']['status-code']);
}, 10_000, 500);
$this->assertEquals(204, $response['headers']['status-code']);
}
public function testSessionAlert(): void
@@ -4160,178 +4160,4 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
/**
* Test that a new email/password session is immediately usable even when
* a concurrent request re-populates the user cache between the cache purge
* and session creation.
*
* Regression test for: purging the user cache BEFORE persisting the session
* allows a concurrent request (from a different Swoole worker) to re-cache
* a stale user document that lacks the new session, causing sessionVerify
* to fail with 401 on subsequent requests using the new session.
*/
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
{
$projectId = $this->getProject()['$id'];
$endpoint = $this->client->getEndpoint();
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
$password = 'password123!';
// Create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Race Test User',
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Login to get session A
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $responseA['headers']['status-code']);
$sessionA = $responseA['cookies']['a_session_' . $projectId];
// Verify session A works
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
]);
$this->assertEquals(200, $verifyA['headers']['status-code']);
/**
* Race condition scenario:
* 1. Start login B via curl_multi (non-blocking)
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
* 3. THEN add GET requests to curl_multi - these hit different workers and
* re-cache a stale user document (without session B) during the window
* between purgeCachedDocument and createDocument
* 4. After all complete, verify session B is usable
*/
for ($attempt = 0; $attempt < 5; $attempt++) {
$loginCookies = [];
$multi = curl_multi_init();
// Start login B first (alone)
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
curl_setopt_array($loginHandle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
],
CURLOPT_POSTFIELDS => \json_encode([
'email' => $email,
'password' => $password,
]),
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
if (\stripos($header, 'set-cookie:') === 0) {
$cookiePart = \trim(\substr($header, 11));
$eqPos = \strpos($cookiePart, '=');
if ($eqPos !== false) {
$name = \substr($cookiePart, 0, $eqPos);
$rest = \substr($cookiePart, $eqPos + 1);
$semiPos = \strpos($rest, ';');
$loginCookies[$name] = $semiPos !== false
? \substr($rest, 0, $semiPos)
: $rest;
}
}
return \strlen($header);
},
]);
curl_multi_add_handle($multi, $loginHandle);
// Drive the login transfer forward and wait for the server to start
// processing the login (past hash verification + cache purge).
$deadline = \microtime(true) + 0.15; // 150ms
do {
curl_multi_exec($multi, $active);
curl_multi_select($multi, 0.005);
} while (\microtime(true) < $deadline && $active);
// NOW add GET requests - they arrive after the cache purge
// but before session creation (which is delayed by the usleep or I/O).
$getHandles = [];
for ($i = 0; $i < 10; $i++) {
$gh = curl_init("{$endpoint}/account");
curl_setopt_array($gh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
"cookie: a_session_{$projectId}={$sessionA}",
],
]);
curl_multi_add_handle($multi, $gh);
$getHandles[] = $gh;
}
// Drive all to completion
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi, 0.05);
}
} while ($active && $status === CURLM_OK);
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($multi, $loginHandle);
curl_close($loginHandle);
foreach ($getHandles as $gh) {
curl_multi_remove_handle($multi, $gh);
curl_close($gh);
}
curl_multi_close($multi);
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
// THE CRITICAL CHECK: verify session B is usable immediately
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
$this->assertEquals(
200,
$verifyB['headers']['status-code'],
'Session B must be immediately usable after login. '
. 'A 401 here means a stale user cache (without the new session) was served. '
. 'The fix is to create the session document BEFORE purging the user cache.'
);
// Clean up session B for next iteration
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
}
}
}
@@ -11539,4 +11539,162 @@ trait DatabasesBase
$this->assertEquals('Product B', $rows['body'][$this->getRecordResource()][0]['name']);
$this->assertEquals(139.99, $rows['body'][$this->getRecordResource()][0]['price']);
}
public function testDocumentWithEmptyPayload(): void
{
$data = $this->setupCollection();
$databaseId = $data['databaseId'];
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$this->getRecordIdParam() => ID::unique(),
'data' => [],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
if ($this->getSupportForAttributes()) {
$this->assertEquals(400, $document['headers']['status-code']);
} else {
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
$this->assertArrayNotHasKey('$collection', $document['body']);
$this->assertEquals($databaseId, $document['body']['$databaseId']);
$this->assertTrue(array_key_exists('$sequence', $document['body']));
$this->assertIsString($document['body']['$sequence']);
$documentId = $document['body']['$id'];
$fetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $fetched['headers']['status-code']);
$this->assertEqualsCanonicalizing([
'$id',
'$databaseId',
'$createdAt',
'$updatedAt',
'$permissions',
'$sequence',
$this->getContainerIdResponseKey(),
], \array_keys($fetched['body']));
$this->assertFalse(array_key_exists('$tenant', $fetched['body']));
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()),
[
'data' => [
'status' => 'draft',
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('draft', $updated['body']['status']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $refetched['headers']['status-code']);
$this->assertEquals('draft', $refetched['body']['status']);
}
}
/**
* API keys may set $createdAt / $updatedAt; invalid strings must return 400, not 500.
* Assertions are HTTP status codes only (no error body matching).
*/
public function testInvalidDate(): void
{
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$invalidDatetime = '1dfs:12:55+sdf:00';
$validUpdatedAt = '2024-01-01T00:00:00Z';
$apiKeyHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$documentPayload = [
'title' => 'Captain America',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
],
];
$permissions = [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
];
$invalidCreate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => \array_merge($documentPayload, ['$updatedAt' => $invalidDatetime]),
'permissions' => $permissions,
]);
$this->assertEquals(400, $invalidCreate['headers']['status-code']);
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => $documentPayload,
'permissions' => $permissions,
]);
$this->assertEquals(201, $document['headers']['status-code']);
$documentId = $document['body']['$id'];
$this->assertNotEmpty($documentId);
$invalidPatch = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $invalidDatetime,
],
]
);
$this->assertEquals(400, $invalidPatch['headers']['status-code']);
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $validUpdatedAt,
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders
);
$this->assertEquals(200, $refetched['headers']['status-code']);
}
}
@@ -3295,6 +3295,277 @@ class RealtimeCustomClientTest extends Scope
$client->close();
}
public function testChannelMirrorEventsAcrossDatabasesAndTablesdb(): void
{
$user = $this->getUser();
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
/**
* Case 1: Trigger event through /databases route and verify both
* legacy collections/documents and tables/rows events are generated.
*/
$legacyDatabase = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'databaseId' => ID::unique(),
'name' => 'Mirror Legacy DB',
]);
$this->assertEquals(201, $legacyDatabase['headers']['status-code']);
$legacyDatabaseId = $legacyDatabase['body']['$id'];
$legacyCollection = $this->client->call(Client::METHOD_POST, '/databases/' . $legacyDatabaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'collectionId' => ID::unique(),
'name' => 'Legacy Collection',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$legacyCollectionId = $legacyCollection['body']['$id'];
$attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $legacyDatabaseId . '/collections/' . $legacyCollectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'key' => 'name',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $attribute['headers']['status-code']);
$this->assertEventually(function () use ($legacyDatabaseId, $legacyCollectionId) {
$attribute = $this->client->call(Client::METHOD_GET, '/databases/' . $legacyDatabaseId . '/collections/' . $legacyCollectionId . '/attributes/name', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals('available', $attribute['body']['status']);
}, 30000, 250);
$legacyClient = $this->getWebsocket([
"databases.{$legacyDatabaseId}.collections.{$legacyCollectionId}.documents",
"databases.{$legacyDatabaseId}.tables.{$legacyCollectionId}.rows",
], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session,
]);
$connected = json_decode($legacyClient->receive(), true);
$this->assertEquals('connected', $connected['type']);
$legacyDocumentId = ID::unique();
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $legacyDatabaseId . '/collections/' . $legacyCollectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'documentId' => $legacyDocumentId,
'data' => [
'name' => 'legacy-route-create',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $document['headers']['status-code']);
$legacyEvent = json_decode($legacyClient->receive(), true);
$this->assertEquals('event', $legacyEvent['type']);
$this->assertContains(
"databases.{$legacyDatabaseId}.collections.{$legacyCollectionId}.documents.{$legacyDocumentId}.create",
$legacyEvent['data']['events']
);
$this->assertContains(
"databases.{$legacyDatabaseId}.tables.{$legacyCollectionId}.rows.{$legacyDocumentId}.create",
$legacyEvent['data']['events']
);
$legacyClient->close();
/**
* Case 2: Trigger event through /tablesdb route and verify both
* tables/rows and legacy collections/documents events are generated.
*/
$tablesDatabase = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'databaseId' => ID::unique(),
'name' => 'Mirror TablesDB',
]);
$this->assertEquals(201, $tablesDatabase['headers']['status-code']);
$tablesDatabaseId = $tablesDatabase['body']['$id'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $tablesDatabaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'tableId' => ID::unique(),
'name' => 'Mirror Table',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
$column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $tablesDatabaseId . '/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'key' => 'name',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
$this->assertEventually(function () use ($tablesDatabaseId, $tableId) {
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $tablesDatabaseId . '/tables/' . $tableId . '/columns/name', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()));
$this->assertEquals('available', $column['body']['status']);
}, 120000, 500);
$tablesClient = $this->getWebsocket([
"databases.{$tablesDatabaseId}.tables.{$tableId}.rows",
"databases.{$tablesDatabaseId}.collections.{$tableId}.documents",
], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session,
]);
$connected = json_decode($tablesClient->receive(), true);
$this->assertEquals('connected', $connected['type']);
$rowId = ID::unique();
$row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $tablesDatabaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'rowId' => $rowId,
'data' => [
'name' => 'tablesdb-route-create',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $row['headers']['status-code']);
$tablesEvent = json_decode($tablesClient->receive(), true);
$this->assertEquals('event', $tablesEvent['type']);
$this->assertContains(
"databases.{$tablesDatabaseId}.tables.{$tableId}.rows.{$rowId}.create",
$tablesEvent['data']['events']
);
$this->assertContains(
"databases.{$tablesDatabaseId}.collections.{$tableId}.documents.{$rowId}.create",
$tablesEvent['data']['events']
);
$tablesClient->close();
/**
* Case 3: Trigger event through /documentsdb route and verify only
* documentsdb events are generated (no databases/tablesdb mirrors).
*/
$documentsDatabase = $this->client->call(Client::METHOD_POST, '/documentsdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'databaseId' => ID::unique(),
'name' => 'Mirror DocumentsDB',
]);
$this->assertEquals(201, $documentsDatabase['headers']['status-code']);
$documentsDatabaseId = $documentsDatabase['body']['$id'];
$documentsCollection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $documentsDatabaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'collectionId' => ID::unique(),
'name' => 'Mirror Documents Collection',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $documentsCollection['headers']['status-code']);
$documentsCollectionId = $documentsCollection['body']['$id'];
$documentsClient = $this->getWebsocket([
"documentsdb.{$documentsDatabaseId}.collections.{$documentsCollectionId}.documents",
'documents',
], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session,
]);
$connected = json_decode($documentsClient->receive(), true);
$this->assertEquals('connected', $connected['type']);
$documentsDocumentId = ID::unique();
$documentsDocument = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $documentsDatabaseId . '/collections/' . $documentsCollectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'documentId' => $documentsDocumentId,
'data' => [
'name' => 'documentsdb-route-create',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $documentsDocument['headers']['status-code']);
$documentsEvent = json_decode($documentsClient->receive(), true);
$this->assertEquals('event', $documentsEvent['type']);
$this->assertContains(
"documentsdb.{$documentsDatabaseId}.collections.{$documentsCollectionId}.documents.{$documentsDocumentId}.create",
$documentsEvent['data']['events']
);
$this->assertEmpty(
array_filter(
$documentsEvent['data']['events'],
fn (string $event) => \str_starts_with($event, 'databases.') || \str_starts_with($event, 'tablesdb.')
)
);
$documentsClient->close();
}
public function testChannelDatabaseTransactionMultipleOperations()
{
$user = $this->getUser();
@@ -3968,7 +4239,16 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
sleep(2);
$this->assertEventually(function () use ($databaseId, $actorsId) {
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/name', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()));
$this->assertEquals(200, $column['headers']['status-code']);
$this->assertEquals('available', $column['body']['status']);
}, 120000, 500);
/**
* Test Document Create
@@ -4683,6 +4963,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
$this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
$this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
$this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.create", $response['data']['events']);
$this->assertEmpty(array_filter($response['data']['events'], fn (string $event) => \str_starts_with($event, 'databases.') || \str_starts_with($event, 'tablesdb.')));
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('Chris Evans', $response['data']['payload']['name']);
@@ -4714,6 +4996,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
$this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.update", $response['data']['events']);
$this->assertEmpty(array_filter($response['data']['events'], fn (string $event) => \str_starts_with($event, 'databases.') || \str_starts_with($event, 'tablesdb.')));
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('Chris Evans 2', $response['data']['payload']['name']);
@@ -5367,8 +5651,6 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertEquals(15, $response['data']['payload']['score']);
sleep(1);
try {
$client->receive();
$this->fail('Should not receive duplicate event');
@@ -5400,8 +5682,6 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertEquals(12, $response['data']['payload']['score']);
sleep(1);
try {
$client->receive();
$this->fail('Should not receive duplicate event');
+60 -1
View File
@@ -5,6 +5,7 @@ namespace Tests\Unit\Event;
use Appwrite\Event\Event;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Document;
require_once __DIR__ . '/../../../app/init.php';
@@ -115,7 +116,7 @@ class EventTest extends TestCase
'rowId' => 'prolog',
]);
$this->assertCount(22, $event);
$this->assertCount(42, $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.prolog.create', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.prolog', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.*.create', $event);
@@ -156,4 +157,62 @@ class EventTest extends TestCase
$this->assertInstanceOf(InvalidArgumentException::class, $th, 'An invalid exception was thrown');
}
}
public function testGenerateMirrorEvents(): void
{
$legacyDatabase = new Document(['type' => 'legacy']);
$tableRowEvents = Event::generateEvents('databases.[databaseId].tables.[tableId].rows.[rowId].update', [
'databaseId' => 'factory-db',
'tableId' => 'assembly',
'rowId' => 'row-123',
], $legacyDatabase);
$this->assertContains('databases.factory-db.collections.assembly.documents.row-123.update', $tableRowEvents);
$collectionDocumentEvents = Event::generateEvents('databases.[databaseId].collections.[collectionId].documents.[documentId].update', [
'databaseId' => 'factory-db',
'collectionId' => 'assembly',
'documentId' => 'doc-123',
], $legacyDatabase);
$this->assertContains('databases.factory-db.tables.assembly.rows.doc-123.update', $collectionDocumentEvents);
$tableColumnEvents = Event::generateEvents('databases.[databaseId].tables.[tableId].columns.[columnId].create', [
'databaseId' => 'factory-db',
'tableId' => 'assembly',
'columnId' => 'status',
], $legacyDatabase);
$this->assertContains('databases.factory-db.collections.assembly.attributes.status.create', $tableColumnEvents);
$collectionAttributeEvents = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].create', [
'databaseId' => 'factory-db',
'collectionId' => 'assembly',
'attributeId' => 'status',
], $legacyDatabase);
$this->assertContains('databases.factory-db.tables.assembly.columns.status.create', $collectionAttributeEvents);
$tablesDb = new Document(['type' => 'tablesdb']);
$tablesDbEvents = Event::generateEvents('databases.[databaseId].tables.[tableId].rows.[rowId].update', [
'databaseId' => 'factory-db',
'tableId' => 'assembly',
'rowId' => 'row-123',
], $tablesDb);
$this->assertContains('databases.factory-db.collections.assembly.documents.row-123.update', $tablesDbEvents);
$this->assertContains('tablesdb.factory-db.tables.assembly.rows.row-123.update', $tablesDbEvents);
$tableIdWithReservedWordEvents = Event::generateEvents('databases.[databaseId].tables.[tableId].rows.[rowId].update', [
'databaseId' => 'factory-db',
'tableId' => 'rows-archive',
'rowId' => 'row-123',
], $legacyDatabase);
$this->assertContains('databases.factory-db.collections.rows-archive.documents.row-123.update', $tableIdWithReservedWordEvents);
$this->assertNotContains('databases.factory-db.collections.documents-archive.documents.row-123.update', $tableIdWithReservedWordEvents);
$documentsDb = new Document(['type' => 'documentsdb']);
$documentsDbEvents = Event::generateEvents('databases.[databaseId].collections.[collectionId].documents.[documentId].update', [
'databaseId' => 'factory-db',
'collectionId' => 'assembly',
'documentId' => 'doc-123',
], $documentsDb);
$this->assertContains('documentsdb.factory-db.collections.assembly.documents.doc-123.update', $documentsDbEvents);
$this->assertNotContains('documentsdb.factory-db.tables.assembly.rows.doc-123.update', $documentsDbEvents);
$this->assertNotContains('databases.factory-db.collections.assembly.documents.doc-123.update', $documentsDbEvents);
}
}
+3 -1
View File
@@ -4,6 +4,7 @@ namespace Tests\Unit\GraphQL;
use Appwrite\GraphQL\Types\Mapper;
use Appwrite\Utopia\Response;
use GraphQL\Type\Definition\NamedType;
use PHPUnit\Framework\TestCase;
use Swoole\Http\Response as SwooleResponse;
@@ -24,6 +25,7 @@ class BuilderTest extends TestCase
{
$model = $this->response->getModel(Response::MODEL_TABLE);
$type = Mapper::model(\ucfirst($model->getType()));
$this->assertEquals('Table', $type->name);
$this->assertInstanceOf(NamedType::class, $type);
$this->assertEquals('Table', $type->name());
}
}