Merge branch '1.8.x' of https://github.com/appwrite/appwrite into joins3

# Conflicts:
#	src/Appwrite/Platform/Workers/Deletes.php
This commit is contained in:
fogelito
2026-01-18 09:06:11 +02:00
88 changed files with 5078 additions and 1642 deletions
+1
View File
@@ -102,6 +102,7 @@ _APP_STATS_RESOURCES_INTERVAL=30
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_INTERVAL_DOMAIN_VERIFICATION=60
_APP_INTERVAL_CLEANUP_STALE_EXECUTIONS=300
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
+11
View File
@@ -1288,6 +1288,17 @@ return [
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
+106
View File
@@ -567,6 +567,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -765,6 +776,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('specification'),
@@ -776,6 +798,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('scopes'),
'type' => Database::VAR_STRING,
@@ -1035,6 +1079,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'$id' => ID::custom('fallbackFile'),
'type' => Database::VAR_STRING,
@@ -1046,6 +1101,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -1200,6 +1266,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('buildRuntime'),
'type' => Database::VAR_STRING,
@@ -1357,6 +1445,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildOutput'),
@@ -2098,6 +2197,13 @@ return [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
+1 -1
View File
@@ -104,7 +104,7 @@ return [
'name' => 'Health',
'subtitle' => 'The Health service allows you to both validate and monitor your Appwrite server\'s health.',
'description' => '/docs/services/health.md',
'controller' => 'api/health.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/health',
File diff suppressed because it is too large Load Diff
+1
View File
@@ -103,6 +103,7 @@ App::post('/v1/teams')
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'labels' => [],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'prefs' => new \stdClass(),
+1
View File
@@ -304,6 +304,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
+32 -11
View File
@@ -6,6 +6,7 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
@@ -59,7 +60,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -802,6 +803,20 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
->setProject($project)
->trigger();
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$resourceType = $type === 'function'
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return true;
} elseif ($type === 'api') {
return false;
@@ -812,8 +827,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView);
}
return false;
}
App::init()
@@ -863,7 +876,9 @@ App::init()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) {
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -871,7 +886,7 @@ App::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1144,14 +1159,16 @@ App::options()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization) {
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1535,13 +1552,15 @@ App::get('/robots.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) {
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1568,13 +1587,15 @@ App::get('/humans.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization) {
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
+4 -1
View File
@@ -181,8 +181,10 @@ const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
@@ -194,6 +196,7 @@ const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_USAGE = 'usage';
+8
View File
@@ -1156,3 +1156,11 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request, A
App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
return new TransactionState($dbForProject, $authorization);
}, ['dbForProject', 'authorization']);
App::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
+12 -3
View File
@@ -29,6 +29,7 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
@@ -473,9 +474,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$roles = $user->getRoles($database->getAuthorization());
$channels = $realtime->connections[$connection]['channels'];
$queries = $realtime->connections[$connection]['queries'] ?? [];
$realtime->unsubscribe($connection);
$realtime->subscribe($projectId, $connection, $roles, $channels);
$realtime->subscribe($projectId, $connection, $roles, $channels, $queries);
}
}
@@ -578,6 +580,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
try {
$queries = Realtime::convertQueries($request->getQuery('queries', []));
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $e->getMessage());
}
/**
* Channels Check
@@ -586,7 +593,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
}
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
$realtime->subscribe($project->getId(), $connection, $roles, $channels, $queries);
$realtime->connections[$connection]['authorization'] = $authorization;
@@ -596,6 +603,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
'type' => 'connected',
'data' => [
'channels' => array_keys($channels),
'queries' => $queries,
'user' => $user
]
]));
@@ -730,7 +738,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
// Preserve authorization before subscribe overwrites the connection array
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
$queries = $realtime->connections[$connection]['queries'];
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels, $queries);
// Restore authorization after subscribe
if ($authorization !== null) {
+8
View File
@@ -490,6 +490,14 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
+1
View File
@@ -880,6 +880,7 @@ services:
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
- _APP_INTERVAL_DOMAIN_VERIFICATION
- _APP_INTERVAL_CLEANUP_STALE_EXECUTIONS
appwrite-task-stats-resources:
container_name: appwrite-task-stats-resources
+43 -3
View File
@@ -4,10 +4,13 @@ namespace Appwrite\Messaging\Adapter;
use Appwrite\Messaging\Adapter as MessagingAdapter;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\RuntimeQuery;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class Realtime extends MessagingAdapter
{
@@ -51,9 +54,10 @@ class Realtime extends MessagingAdapter
* @param mixed $identifier
* @param array $roles
* @param array $channels
* @param array $queries
* @return void
*/
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels, array $queries = []): void
{
if (!isset($this->subscriptions[$projectId])) { // Init Project
$this->subscriptions[$projectId] = [];
@@ -72,7 +76,8 @@ class Realtime extends MessagingAdapter
$this->connections[$identifier] = [
'projectId' => $projectId,
'roles' => $roles,
'channels' => $channels
'channels' => $channels,
'queries' => $queries
];
}
@@ -206,7 +211,14 @@ class Realtime extends MessagingAdapter
/**
* To prevent duplicates, we save the connections as array keys.
*/
$receivers[$id] = 0;
$queries = $this->connections[$id]['queries'] ?? [];
$payload = $event['data']['payload'] ?? [];
if (
empty($queries) ||
!empty(RuntimeQuery::filter($queries, $payload))
) {
$receivers[$id] = 0;
}
}
break;
}
@@ -245,6 +257,34 @@ class Realtime extends MessagingAdapter
return $channels;
}
/**
* Converts the queries from the Query Params into an array.
* @param array $queries
* @return array
*/
public static function convertQueries(array $queries): array
{
$queries = Query::parseQueries($queries);
$stack = $queries;
$allowedMethods = implode(', ', RuntimeQuery::ALLOWED_QUERIES);
while (!empty($stack)) {
/** `@var` Query $query */
$query = array_pop($stack);
$method = $query->getMethod();
if (!in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
$unsupportedMethod = $method;
throw new QueryException(
"Query method '{$unsupportedMethod}' is not supported in Realtime queries. Allowed query methods are: {$allowedMethods}"
);
}
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
$stack = array_merge($stack, $query->getValues());
}
}
return $queries;
}
/**
* Create channels array based on the event name and payload.
*
+2
View File
@@ -8,6 +8,7 @@ use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
@@ -25,6 +26,7 @@ class Appwrite extends Platform
$this->addModule(new Databases\Module());
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Health\Module());
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
@@ -107,6 +107,7 @@ class Base extends Action
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $function->getAttribute('commands', ''),
'startCommand' => $function->getAttribute('startCommand', ''),
'type' => 'vcs',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getSequence(),
@@ -203,6 +204,7 @@ class Base extends Action
'resourceInternalId' => $site->getSequence(),
'resourceType' => 'sites',
'buildCommands' => implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
@@ -246,6 +246,7 @@ class Create extends Action
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
@@ -283,6 +284,7 @@ class Create extends Action
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
@@ -104,6 +104,7 @@ class Create extends Action
'totalSize' => $deployment->getAttribute('sourceSize', 0),
'entrypoint' => $function->getAttribute('entrypoint'),
'buildCommands' => $function->getAttribute('commands', ''),
'startCommand' => $function->getAttribute('startCommand', ''),
'buildStartedAt' => null,
'buildEndedAt' => null,
'buildDuration' => null,
@@ -159,6 +159,7 @@ class Create extends Base
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'startCommand' => $function->getAttribute('startCommand', ''),
'providerRepositoryName' => $repository,
'providerRepositoryOwner' => $owner,
'providerRepositoryUrl' => $repositoryUrl,
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
@@ -62,7 +63,6 @@ class Create extends Base
->label('scope', 'execution.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].executions.[executionId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'executions',
@@ -101,6 +101,8 @@ class Create extends Base
->inject('executor')
->inject('platform')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->callback($this->action(...));
}
@@ -127,6 +129,8 @@ class Create extends Base
Executor $executor,
array $platform,
Authorization $authorization,
DeleteEvent $queueForDeletes,
int $executionsRetentionCount,
) {
$async = \strval($async) === 'true' || \strval($async) === '1';
@@ -164,6 +168,7 @@ class Create extends Base
throw new Exception($validator->getDescription(), 400);
}
/* @var Document $function */
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = User::isApp($authorization->getRoles());
@@ -340,6 +345,13 @@ class Create extends Base
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
$queueForDeletes
);
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
@@ -513,8 +525,32 @@ class Create extends Base
}
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
$queueForDeletes
);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
private function enqueueDeletes(
Document $project,
string $resourceId,
int $executionsRetentionCount,
DeleteEvent $queueForDeletes
): void {
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
->setProject($project)
->setResource($resourceId)
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
}
}
@@ -223,6 +223,8 @@ class Create extends Base
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'deploymentRetention' => 0,
'startCommand' => '',
'search' => implode(' ', [$functionId, $name, $runtime]),
'version' => 'v5',
'installationId' => $installation->getId(),
@@ -233,7 +235,9 @@ class Create extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
]));
} catch (DuplicateException) {
throw new Exception(Exception::FUNCTION_ALREADY_EXISTS);
@@ -343,6 +347,7 @@ class Create extends Base
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'startCommand' => $function->getAttribute('startCommand', ''),
'type' => 'manual',
'activate' => true,
]));
@@ -261,6 +261,8 @@ class Update extends Base
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'deploymentRetention' => 0,
'startCommand' => '',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getSequence(),
'providerRepositoryId' => $providerRepositoryId,
@@ -270,6 +272,8 @@ class Update extends Base
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'search' => implode(' ', [$functionId, $name, $runtime]),
])));
@@ -0,0 +1,78 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\AntiVirus;
use Appwrite\ClamAV\Network;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getAntivirus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/anti-virus')
->desc('Get antivirus')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getAntivirus',
description: '/docs/references/health/get-storage-anti-virus.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_ANTIVIRUS,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$output = [
'status' => '',
'version' => '',
];
if (System::getEnv('_APP_STORAGE_ANTIVIRUS') === 'disabled') {
$output['status'] = 'disabled';
$output['version'] = '';
} else {
$antivirus = new Network(
System::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) System::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
try {
$output['version'] = @$antivirus->version();
$output['status'] = (@$antivirus->ping()) ? 'pass' : 'fail';
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Antivirus is not available');
}
}
$response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Cache;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Pools\Group;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCache';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/cache')
->desc('Get cache')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getCache',
description: '/docs/references/health/get-cache.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS_LIST,
)
],
contentType: ContentType::JSON
))
->inject('response')
->inject('pools')
->callback($this->action(...));
}
public function action(Response $response, Group $pools): void
{
$output = [];
$failures = [];
$configs = [
'Cache' => Config::getParam('pools-cache'),
];
foreach ($configs as $key => $config) {
foreach ($config as $cache) {
try {
$adapter = new CachePool($pools->get($cache));
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($cache)",
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) * 1000),
]);
} else {
$failures[] = $cache;
}
} catch (\Throwable) {
$failures[] = $cache;
}
}
}
if (!empty($failures)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Cache failure on: ' . \implode(', ', $failures));
}
$response->dynamic(new Document([
'statuses' => $output,
'total' => \count($output),
]), Response::MODEL_HEALTH_STATUS_LIST);
}
}
@@ -0,0 +1,120 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain;
use Utopia\Validator\Multiple;
use Utopia\Validator\URL;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCertificate';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/certificate')
->desc('Get the SSL certificate for a domain')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getCertificate',
description: '/docs/references/health/get-certificate.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_CERTIFICATE,
)
],
contentType: ContentType::JSON
))
->param('domain', null, new Multiple([new AnyOf([new URL(), new Domain()]), new PublicDomain()]), Multiple::TYPE_STRING, 'Domain name')
->inject('response')
->callback($this->action(...));
}
public function action(string $domain, Response $response): void
{
if (filter_var($domain, FILTER_VALIDATE_URL)) {
$domain = parse_url($domain, PHP_URL_HOST);
}
$sslContext = stream_context_create([
'ssl' => [
'capture_peer_cert' => true,
'SNI_enabled' => true,
'peer_name' => $domain,
],
]);
$sslSocket = @stream_socket_client('ssl://' . $domain . ':443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext);
if (!$sslSocket) {
throw new Exception(Exception::HEALTH_INVALID_HOST, 'Failed to connect to host: (' . ($errno ?? 'unknown') . ') ' . ($errstr ?? 'unknown'));
}
try {
$streamContextParams = stream_context_get_params($sslSocket);
$peerCertificate = $streamContextParams['options']['ssl']['peer_certificate'] ?? null;
if ($peerCertificate === null) {
throw new Exception(Exception::HEALTH_INVALID_HOST, 'Peer certificate not available for ' . $domain);
}
$certificatePayload = @openssl_x509_parse($peerCertificate);
if ($certificatePayload === false || !\is_array($certificatePayload)) {
throw new Exception(Exception::HEALTH_INVALID_HOST, 'Failed to parse peer certificate for ' . $domain);
}
$validFrom = $certificatePayload['validFrom_time_t'] ?? null;
$validTo = $certificatePayload['validTo_time_t'] ?? null;
if ($validFrom === null || $validTo === null) {
throw new Exception(Exception::HEALTH_INVALID_HOST, 'Certificate missing validity period for ' . $domain);
}
$sslExpiration = $validTo;
$status = $sslExpiration < time() ? 'fail' : 'pass';
if ($status === 'fail') {
throw new Exception(Exception::HEALTH_CERTIFICATE_EXPIRED);
}
$name = $certificatePayload['name'] ?? null;
if (empty($name) && !empty($certificatePayload['subject']['CN'])) {
$name = '/CN=' . $certificatePayload['subject']['CN'];
}
$response->dynamic(new Document([
'name' => $name ?? '',
'subjectSN' => $certificatePayload['subject']['CN'] ?? '',
'issuerOrganisation' => $certificatePayload['issuer']['O'] ?? '',
'validFrom' => $validFrom,
'validTo' => $validTo,
'signatureTypeSN' => $certificatePayload['signatureTypeSN'] ?? '',
]), Response::MODEL_HEALTH_CERTIFICATE);
} finally {
@fclose($sslSocket);
}
}
}
@@ -0,0 +1,100 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\DB;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Pools\Group;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getDB';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/db')
->desc('Get DB')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getDB',
description: '/docs/references/health/get-db.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS_LIST,
)
],
contentType: ContentType::JSON
))
->inject('response')
->inject('pools')
->inject('authorization')
->callback($this->action(...));
}
public function action(Response $response, Group $pools, Authorization $authorization): void
{
$output = [];
$failures = [];
$configs = [
'Console.DB' => Config::getParam('pools-console'),
'Projects.DB' => Config::getParam('pools-database'),
];
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
$adapter = new DatabasePool($pools->get($database));
$adapter->setAuthorization($authorization);
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($database)",
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) * 1000)
]);
} else {
$failures[] = $database;
}
} catch (\Throwable) {
$failures[] = $database;
}
}
}
// Only throw error if ALL databases failed (no successful pings)
// This allows partial failures in environments where not all DBs are ready
if (!empty($failures)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'DB failure on: ' . implode(", ", $failures));
}
$response->dynamic(new Document([
'statuses' => $output,
'total' => count($output),
]), Response::MODEL_HEALTH_STATUS_LIST);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'get';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health')
->desc('Get HTTP')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'get',
description: '/docs/references/health/get.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$response->dynamic(new Document([
'name' => 'http',
'status' => 'pass',
'ping' => 0,
]), Response::MODEL_HEALTH_STATUS);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\PubSub;
use Appwrite\Extend\Exception;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Pools\Group;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getPubSub';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/pubsub')
->desc('Get pubsub')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getPubSub',
description: '/docs/references/health/get-pubsub.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS_LIST,
)
],
contentType: ContentType::JSON
))
->inject('response')
->inject('pools')
->callback($this->action(...));
}
public function action(Response $response, Group $pools): void
{
$output = [];
$failures = [];
$configs = [
'PubSub' => Config::getParam('pools-pubsub'),
];
foreach ($configs as $key => $config) {
foreach ($config as $pubsub) {
try {
$adapter = new PubSubPool($pools->get($pubsub));
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($pubsub)",
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) * 1000),
]);
} else {
$failures[] = $pubsub;
}
} catch (\Throwable) {
$failures[] = $pubsub;
}
}
}
if (!empty($failures)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Pubsub failure on: ' . \implode(', ', $failures));
}
$response->dynamic(new Document([
'statuses' => $output,
'total' => \count($output),
]), Response::MODEL_HEALTH_STATUS_LIST);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits;
use Appwrite\Event\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueAudits';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/audits')
->desc('Get audits queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueAudits',
description: '/docs/references/health/get-queue-audits.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
abstract class Base extends Action
{
use HTTP;
protected function assertQueueThreshold(int $size, int $threshold, bool $failed = false): void
{
if ($size >= $threshold) {
$context = $failed ? 'failed jobs' : 'jobs';
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue {$context} threshold hit. Current value is {$size} and threshold is {$threshold}.");
}
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Builds;
use Appwrite\Event\Build;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueBuilds';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/builds')
->desc('Get builds queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueBuilds',
description: '/docs/references/health/get-queue-builds.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('queueForBuilds')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Build $queueForBuilds, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForBuilds->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates;
use Appwrite\Event\Certificate;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueCertificates';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/certificates')
->desc('Get certificates queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueCertificates',
description: '/docs/references/health/get-queue-certificates.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Certificate $queueForCertificates, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForCertificates->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Databases;
use Appwrite\Event\Database;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueDatabases';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/databases')
->desc('Get databases queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueDatabases',
description: '/docs/references/health/get-queue-databases.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForDatabase')
->inject('response')
->callback($this->action(...));
}
public function action(string $name, int|string $threshold, Database $queueForDatabase, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForDatabase->setQueue($name)->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Deletes;
use Appwrite\Event\Delete;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueDeletes';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/deletes')
->desc('Get deletes queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueDeletes',
description: '/docs/references/health/get-queue-deletes.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForDeletes')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Delete $queueForDeletes, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForDeletes->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,133 @@
<?php
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\Migration;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\System\System;
use Utopia\Validator\Integer;
use Utopia\Validator\WhiteList;
class Get extends Base
{
public static function getName(): string
{
return 'getFailedJobs';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/failed/:name')
->desc('Get number of failed queue jobs')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getFailedJobs',
description: '/docs/references/health/get-failed-queue-jobs.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('name', '', new WhiteList([
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME),
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME),
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME),
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME),
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME),
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME),
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME),
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME),
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME),
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME),
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME),
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME),
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME),
]), 'The name of the queue')
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('response')
->inject('queueForDatabase')
->inject('queueForDeletes')
->inject('queueForAudits')
->inject('queueForMails')
->inject('queueForFunctions')
->inject('queueForStatsResources')
->inject('queueForStatsUsage')
->inject('queueForWebhooks')
->inject('queueForCertificates')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('queueForMigrations')
->inject('queueForScreenshots')
->callback($this->action(...));
}
public function action(
string $name,
int|string $threshold,
Response $response,
Database $queueForDatabase,
Delete $queueForDeletes,
Audit $queueForAudits,
Mail $queueForMails,
Func $queueForFunctions,
StatsResources $queueForStatsResources,
StatsUsage $queueForStatsUsage,
Webhook $queueForWebhooks,
Certificate $queueForCertificates,
Build $queueForBuilds,
Messaging $queueForMessaging,
Migration $queueForMigrations,
Screenshot $queueForScreenshots,
): 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_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) => $queueForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $queueForStatsUsage,
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_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations,
};
$failed = $queue->getSize(failed: true);
$this->assertQueueThreshold($failed, $threshold, true);
$response->dynamic(new Document(['size' => $failed]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Functions;
use Appwrite\Event\Func;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueFunctions';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/functions')
->desc('Get functions queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueFunctions',
description: '/docs/references/health/get-queue-functions.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForFunctions')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Func $queueForFunctions, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForFunctions->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs;
use Appwrite\Event\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueLogs';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/logs')
->desc('Get logs queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueLogs',
description: '/docs/references/health/get-queue-logs.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Mails;
use Appwrite\Event\Mail;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueMails';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/mails')
->desc('Get mails queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueMails',
description: '/docs/references/health/get-queue-mails.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForMails')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Mail $queueForMails, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMails->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Messaging;
use Appwrite\Event\Messaging;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueMessaging';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/messaging')
->desc('Get messaging queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueMessaging',
description: '/docs/references/health/get-queue-messaging.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForMessaging')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Messaging $queueForMessaging, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMessaging->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Migrations;
use Appwrite\Event\Migration;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueMigrations';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/migrations')
->desc('Get migrations queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueMigrations',
description: '/docs/references/health/get-queue-migrations.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('queueForMigrations')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Migration $queueForMigrations, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMigrations->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsResources;
use Appwrite\Event\StatsResources;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueStatsResources';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/stats-resources')
->desc('Get stats resources queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueStatsResources',
description: '/docs/references/health/get-queue-stats-resources.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('queueForStatsResources')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, StatsResources $queueForStatsResources, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForStatsResources->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsUsage;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueUsage';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/stats-usage')
->desc('Get stats usage queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueUsage',
description: '/docs/references/health/get-queue-stats-usage.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('queueForStatsUsage')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, StatsUsage $queueForStatsUsage, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForStatsUsage->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Webhooks;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Validator\Integer;
class Get extends Base
{
public static function getName(): string
{
return 'getQueueWebhooks';
}
public function __construct()
{
$this
->setHttpMethod(Base::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/queue/webhooks')
->desc('Get webhooks queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'queue',
name: 'getQueueWebhooks',
description: '/docs/references/health/get-queue-webhooks.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
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('queueForWebhooks')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Webhook $queueForWebhooks, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForWebhooks->getSize();
$this->assertQueueThreshold($size, $threshold);
$response->dynamic(new Document(['size' => $size]), Response::MODEL_HEALTH_QUEUE);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Stats;
use Appwrite\Utopia\Response;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Storage;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getStats';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/stats')
->desc('Get system stats')
->groups(['api', 'health'])
->label('scope', 'root')
->label('docs', false)
->inject('response')
->inject('register')
->inject('deviceForFiles')
->callback($this->action(...));
}
public function action(Response $response, Registry $register, Device $deviceForFiles): void
{
$cache = $register->get('cache');
$cacheStats = $cache->info();
$response->json([
'storage' => [
'used' => Storage::human($deviceForFiles->getDirectorySize($deviceForFiles->getRoot() . '/')),
'partitionTotal' => Storage::human($deviceForFiles->getPartitionTotalSpace()),
'partitionFree' => Storage::human($deviceForFiles->getPartitionFreeSpace()),
],
'cache' => [
'uptime' => $cacheStats['uptime_in_seconds'] ?? 0,
'clients' => $cacheStats['connected_clients'] ?? 0,
'hits' => $cacheStats['keyspace_hits'] ?? 0,
'misses' => $cacheStats['keyspace_misses'] ?? 0,
'memory_used' => $cacheStats['used_memory'] ?? 0,
'memory_used_human' => $cacheStats['used_memory_human'] ?? 0,
'memory_used_peak' => $cacheStats['used_memory_peak'] ?? 0,
'memory_used_peak_human' => $cacheStats['used_memory_peak_human'] ?? 0,
],
]);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Storage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getStorage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/storage')
->desc('Get storage')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'storage',
name: 'getStorage',
description: '/docs/references/health/get-storage.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS,
)
],
contentType: ContentType::JSON
))
->inject('response')
->inject('deviceForFiles')
->inject('deviceForFunctions')
->inject('deviceForSites')
->inject('deviceForBuilds')
->callback($this->action(...));
}
public function action(Response $response, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds): void
{
$devices = [$deviceForFiles, $deviceForFunctions, $deviceForSites, $deviceForBuilds];
$checkStart = \microtime(true);
foreach ($devices as $device) {
$uniqueFileName = \uniqid('health', true);
$filePath = $device->getPath($uniqueFileName);
if (!$device->write($filePath, 'test', 'text/plain')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
}
$readError = null;
try {
if ($device->read($filePath) !== 'test') {
$readError = new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
}
} catch (\Throwable $e) {
$readError = $e;
} finally {
// Always attempt to clean up test file
if (!$device->delete($filePath)) {
if ($readError !== null) {
// If read already failed, wrap delete error but preserve original
\error_log('Failed deleting test file from ' . $device->getRoot() . ' during read error recovery');
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
}
}
// Re-throw read error if it occurred
if ($readError !== null) {
throw $readError;
}
}
}
$response->dynamic(new Document([
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) * 1000),
]), Response::MODEL_HEALTH_STATUS);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Storage\Local;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device\Local;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getStorageLocal';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/storage/local')
->desc('Get local storage')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'storage',
name: 'getStorageLocal',
description: '/docs/references/health/get-storage-local.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$checkStart = \microtime(true);
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES,
] as $key => $volume
) {
$device = new Local($volume);
if (!\is_readable($device->getRoot())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Device ' . $key . ' dir is not readable');
}
if (!\is_writable($device->getRoot())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Device ' . $key . ' dir is not writable');
}
}
$response->dynamic(new Document([
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) * 1000),
]), Response::MODEL_HEALTH_STATUS);
}
}
@@ -0,0 +1,107 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Time;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getTime';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/time')
->desc('Get time')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
namespace: 'health',
group: 'health',
name: 'getTime',
description: '/docs/references/health/get-time.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_TIME,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$host = 'time.google.com';
$gap = 60;
$sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($sock === false) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to create socket: ' . \socket_strerror(\socket_last_error()));
}
try {
if (!\socket_connect($sock, $host, 123)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to connect to time server: ' . \socket_strerror(\socket_last_error($sock)));
}
// Set receive timeout to prevent hanging
if (!\socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 5, 'usec' => 0])) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to set socket timeout: ' . \socket_strerror(\socket_last_error($sock)));
}
$msg = "\010" . \str_repeat("\0", 47);
$sent = \socket_send($sock, $msg, \strlen($msg), 0);
if ($sent === false) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to send NTP request: ' . \socket_strerror(\socket_last_error($sock)));
}
$recv = false;
if (!\socket_recv($sock, $recv, 48, MSG_WAITALL)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to receive NTP response: ' . \socket_strerror(\socket_last_error($sock)));
}
if ($recv === false || \strlen($recv) !== 48) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid NTP response: received ' . (\is_string($recv) ? \strlen($recv) : 'no') . ' bytes instead of 48');
}
$data = \unpack('N12', $recv);
$timestamp = \sprintf('%u', $data[9]);
$timestamp -= 2208988800;
$diff = ($timestamp - \time());
if ($diff > $gap || $diff < ($gap * -1)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Server time gaps detected');
}
$response->dynamic(new Document([
'remoteTime' => $timestamp,
'localTime' => \time(),
'diff' => $diff,
]), Response::MODEL_HEALTH_TIME);
} finally {
\socket_close($sock);
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Appwrite\Platform\Modules\Health\Http\Health\Version;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getVersion';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/health/version')
->desc('Get version')
->groups(['api', 'health'])
->label('scope', 'public')
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$response->dynamic(new Document(['version' => APP_VERSION_STABLE]), Response::MODEL_HEALTH_VERSION);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Health\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Platform\Modules\Health\Services;
use Appwrite\Platform\Modules\Health\Http\Health\AntiVirus\Get as GetAntivirus;
use Appwrite\Platform\Modules\Health\Http\Health\Cache\Get as GetCache;
use Appwrite\Platform\Modules\Health\Http\Health\Certificate\Get as GetCertificate;
use Appwrite\Platform\Modules\Health\Http\Health\DB\Get as GetDB;
use Appwrite\Platform\Modules\Health\Http\Health\Get as GetHealth;
use Appwrite\Platform\Modules\Health\Http\Health\PubSub\Get as GetPubSub;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits\Get as GetQueueAudits;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Builds\Get as GetQueueBuilds;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates\Get as GetQueueCertificates;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Databases\Get as GetQueueDatabases;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Deletes\Get as GetQueueDeletes;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed\Get as GetFailedJobs;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Functions\Get as GetQueueFunctions;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs\Get as GetQueueLogs;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Mails\Get as GetQueueMails;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Messaging\Get as GetQueueMessaging;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Migrations\Get as GetQueueMigrations;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsResources\Get as GetQueueStatsResources;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsUsage\Get as GetQueueUsage;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Webhooks\Get as GetQueueWebhooks;
use Appwrite\Platform\Modules\Health\Http\Health\Stats\Get as GetStats;
use Appwrite\Platform\Modules\Health\Http\Health\Storage\Get as GetStorage;
use Appwrite\Platform\Modules\Health\Http\Health\Storage\Local\Get as GetStorageLocal;
use Appwrite\Platform\Modules\Health\Http\Health\Time\Get as GetTime;
use Appwrite\Platform\Modules\Health\Http\Health\Version\Get as GetHealthVersion;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
$this->addAction(GetHealth::getName(), new GetHealth());
$this->addAction(GetHealthVersion::getName(), new GetHealthVersion());
$this->addAction(GetDB::getName(), new GetDB());
$this->addAction(GetCache::getName(), new GetCache());
$this->addAction(GetPubSub::getName(), new GetPubSub());
$this->addAction(GetTime::getName(), new GetTime());
$this->addAction(GetCertificate::getName(), new GetCertificate());
$this->addAction(GetStorageLocal::getName(), new GetStorageLocal());
$this->addAction(GetStorage::getName(), new GetStorage());
$this->addAction(GetAntivirus::getName(), new GetAntivirus());
$this->addAction(GetQueueAudits::getName(), new GetQueueAudits());
$this->addAction(GetQueueWebhooks::getName(), new GetQueueWebhooks());
$this->addAction(GetQueueLogs::getName(), new GetQueueLogs());
$this->addAction(GetQueueCertificates::getName(), new GetQueueCertificates());
$this->addAction(GetQueueBuilds::getName(), new GetQueueBuilds());
$this->addAction(GetQueueDatabases::getName(), new GetQueueDatabases());
$this->addAction(GetQueueDeletes::getName(), new GetQueueDeletes());
$this->addAction(GetQueueMails::getName(), new GetQueueMails());
$this->addAction(GetQueueMessaging::getName(), new GetQueueMessaging());
$this->addAction(GetQueueMigrations::getName(), new GetQueueMigrations());
$this->addAction(GetQueueFunctions::getName(), new GetQueueFunctions());
$this->addAction(GetQueueStatsResources::getName(), new GetQueueStatsResources());
$this->addAction(GetQueueUsage::getName(), new GetQueueUsage());
$this->addAction(GetFailedJobs::getName(), new GetFailedJobs());
$this->addAction(GetStats::getName(), new GetStats());
}
}
@@ -253,6 +253,7 @@ class Create extends Action
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
@@ -320,6 +321,7 @@ class Create extends Action
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
@@ -119,6 +119,7 @@ class Create extends Action
'sourcePath' => $destination,
'totalSize' => $deployment->getAttribute('sourceSize', 0),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
@@ -166,6 +166,7 @@ class Create extends Base
'resourceInternalId' => $site->getSequence(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'providerRepositoryName' => $repository,
'providerRepositoryOwner' => $owner,
@@ -150,6 +150,8 @@ class Create extends Base
'timeout' => $timeout,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'deploymentRetention' => 0,
'startCommand' => '',
'outputDirectory' => $outputDirectory,
'search' => implode(' ', [$siteId, $name, $framework]),
'fallbackFile' => $fallbackFile,
@@ -162,6 +164,8 @@ class Create extends Base
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
]));
@@ -254,6 +254,8 @@ class Update extends Base
'timeout' => $timeout,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'deploymentRetention' => 0,
'startCommand' => '',
'outputDirectory' => $outputDirectory,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getSequence(),
@@ -264,6 +266,8 @@ class Update extends Base
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'search' => implode(' ', [$siteId, $name, $framework]),
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
+52 -1
View File
@@ -24,22 +24,30 @@ class Interval extends Action
$this
->desc('Schedules tasks on regular intervals by publishing them to our queues')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForCertificates')
->callback($this->action(...));
}
public function action(Database $dbForPlatform, Certificate $queueForCertificates): void
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): void
{
Console::title('Interval V1');
Console::success(APP_NAME . ' interval process v1 has started');
$intervalDomainVerification = (int) System::getEnv('_APP_INTERVAL_DOMAIN_VERIFICATION', '60'); // 1 minute
$intervalCleanupStaleExecutions = (int) System::getEnv('_APP_INTERVAL_CLEANUP_STALE_EXECUTIONS', '300'); // 5 minutes
\go(function () use ($dbForPlatform, $queueForCertificates, $intervalDomainVerification) {
Console::loop(function () use ($dbForPlatform, $queueForCertificates) {
$this->verifyDomain($dbForPlatform, $queueForCertificates);
}, $intervalDomainVerification);
});
\go(function () use ($dbForPlatform, $getProjectDB, $intervalCleanupStaleExecutions) {
Console::loop(function () use ($dbForPlatform, $getProjectDB) {
$this->cleanupStaleExecutions($dbForPlatform, $getProjectDB);
}, $intervalCleanupStaleExecutions);
});
}
private function verifyDomain(Database $dbForPlatform, Certificate $queueForCertificates): void
@@ -72,4 +80,47 @@ class Interval extends Action
->trigger();
}
}
private function cleanupStaleExecutions(Database $dbForPlatform, callable $getProjectDB): void
{
$time = DatabaseDateTime::now();
$staleThreshold = DatabaseDateTime::addSeconds(new DateTime(), -1200); // 20 minutes ago
Console::info("[{$time}] Starting cleanup of stale executions");
$dbForPlatform->foreach(
'projects',
function (Document $project) use ($getProjectDB, $time, $staleThreshold) {
try {
$dbForProject = $getProjectDB($project);
$staleExecutions = $dbForProject->find('executions', [
Query::equal('status', ['processing']),
Query::lessThan('$createdAt', $staleThreshold),
Query::limit(100),
]);
if (\count($staleExecutions) === 0) {
return;
}
Console::info("[{$time}] Found " . \count($staleExecutions) . " stale executions in project {$project->getId()}");
foreach ($staleExecutions as $execution) {
$execution->setAttribute('status', 'failed');
$execution->setAttribute('errors', 'Execution timed out');
$dbForProject->updateDocument('executions', $execution->getId(), $execution);
}
} catch (\Throwable $th) {
Console::error("[{$time}] Failed to cleanup stale executions for project {$project->getId()}: " . $th->getMessage());
}
},
[
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::limit(100),
]
);
Console::info("[{$time}] Completed cleanup of stale executions");
}
}
+101 -8
View File
@@ -30,6 +30,8 @@ use Utopia\Queue\Message;
use Utopia\Storage\Device;
use Utopia\System\System;
use function Swoole\Coroutine\batch;
class Deletes extends Action
{
protected array $selects = ['$sequence', '$id', '$collection', '$permissions', '$updatedAt'];
@@ -59,6 +61,7 @@ class Deletes extends Action
->inject('certificates')
->inject('executor')
->inject('executionRetention')
->inject('executionsRetentionCount')
->inject('auditRetention')
->inject('log')
->inject('getAudit')
@@ -83,6 +86,7 @@ class Deletes extends Action
CertificatesAdapter $certificates,
Executor $executor,
string $executionRetention,
int $executionsRetentionCount,
string $auditRetention,
Log $log,
callable $getAudit,
@@ -144,6 +148,19 @@ class Deletes extends Action
case DELETE_TYPE_EXECUTIONS:
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention);
break;
case DELETE_TYPE_EXECUTIONS_LIMIT:
$resourceInternalId = $payload['resource'] ?? null;
$resourceType = $payload['resourceType'] ?? null;
if ($resourceInternalId) {
$this->deleteExecutionsByLimit(
$project,
$getProjectDB,
$executionsRetentionCount,
$resourceInternalId,
$resourceType
);
}
break;
case DELETE_TYPE_AUDIT:
if (!$project->isEmpty()) {
$this->deleteAuditLogs($project, $getAudit, $auditRetention);
@@ -184,7 +201,7 @@ class Deletes extends Action
break;
case DELETE_TYPE_MAINTENANCE:
$this->deleteExpiredTargets($project, $getProjectDB);
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention);
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention, $executionsRetentionCount);
$this->deleteAuditLogs($project, $getAudit, $auditRetention);
$this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime);
$this->deleteExpiredSessions($project, $getProjectDB);
@@ -199,16 +216,15 @@ class Deletes extends Action
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @param Document|null $document
* @return void
* @throws Conflict
* @throws Restricted
* @throws Structure
* @throws DatabaseException
* @throws DatabaseException|Exception
*/
private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void
{
// Temporarly accepting both 'fra' and 'default'
// Temporarily accepting both 'fra' and 'default'
// When all migrated, only use _APP_REGION with 'default' as default value
$regions = [System::getEnv('_APP_REGION', 'default')];
if (!in_array('default', $regions)) {
@@ -711,14 +727,16 @@ class Deletes extends Action
}
/**
* @param database $dbForPlatform
* @param Document $project
* @param callable $getProjectDB
* @param string $datetime
* @param int|null $executionsRetentionCount
* @return void
* @throws Exception
* @throws Exception|DatabaseException
*/
private function deleteExecutionLogs(Document $project, callable $getProjectDB, string $datetime): void
private function deleteExecutionLogs(Document $project, callable $getProjectDB, string $datetime, ?int $executionsRetentionCount = 0): void
{
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
$queries = [
@@ -734,10 +752,85 @@ class Deletes extends Action
// Delete Executions
$this->deleteByGroup('executions', $queries, $dbForProject);
/* delete based on custom retention, if any */
$this->deleteExecutionsByLimit($project, $getProjectDB, $executionsRetentionCount);
}
/**
* @param Database $dbForPlatform
* @param Document $project
* @param callable $getProjectDB
* @param int|null $executionsRetentionCount
* @param string|null $resourceInternalId
* @param string|null $resourceType
* @return void
* @throws DatabaseException
*/
protected function deleteExecutionsByLimit(
Document $project,
callable $getProjectDB,
?int $executionsRetentionCount = 0,
?string $resourceInternalId = null,
?string $resourceType = null
): void {
if ($executionsRetentionCount <= 0) {
return;
}
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
/* delete log for a given $resourceInternalId */
$delete = function (Database $dbForProject, string $resourceInternalId, string $resourceType) use ($executionsRetentionCount) {
// get the execution at position `N+1`
$execution = $dbForProject->findOne('executions', [
Query::select(['$createdAt']),
Query::equal('resourceInternalId', [$resourceInternalId]),
Query::equal('resourceType', [$resourceType]),
Query::orderDesc('$createdAt'),
Query::offset($executionsRetentionCount),
]);
if (!$execution->isEmpty()) {
// delete everything older
$cutoffTime = $execution->getAttribute('$createdAt');
$this->deleteByGroup('executions', [
Query::select([...$this->selects, '$createdAt']),
Query::equal('resourceInternalId', [$resourceInternalId]),
Query::equal('resourceType', [$resourceType]),
Query::lessThan('$createdAt', $cutoffTime),
Query::orderDesc('$createdAt'),
Query::orderDesc(),
], $dbForProject);
}
};
if (!empty($resourceInternalId)) {
// fast path, no need to list anything!
$delete($dbForProject, $resourceInternalId, $resourceType);
} else {
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
$this->listByGroup(
collection: $type,
queries: [Query::select(['$id', '$sequence'])],
database: $dbForProject,
callback: function (Document $resource) use ($dbForProject, $delete, $type) {
$delete($dbForProject, $resource->getSequence(), $type);
}
);
};
/* perform processing in parallel */
batch([
fn () => $processResource(RESOURCE_TYPE_SITES),
fn () => $processResource(RESOURCE_TYPE_FUNCTIONS),
]);
}
}
/**
* @param Document $project
* @param callable $getProjectDB
* @return void
* @throws Exception|Throwable
@@ -0,0 +1,121 @@
<?php
namespace Appwrite\Utopia\Database;
use Utopia\Database\Query;
class RuntimeQuery extends Query
{
public const ALLOWED_QUERIES = [
// Equality & comparison
Query::TYPE_EQUAL,
Query::TYPE_NOT_EQUAL,
Query::TYPE_LESSER,
Query::TYPE_LESSER_EQUAL,
Query::TYPE_GREATER,
Query::TYPE_GREATER_EQUAL,
// Null checks
Query::TYPE_IS_NULL,
Query::TYPE_IS_NOT_NULL,
// Recursive checks
Query::TYPE_AND,
Query::TYPE_OR
];
/**
* @param array<Query> $queries
* @param array<string, mixed> $payload
*/
public static function filter(array $queries, array $payload): array
{
if (empty($queries)) {
return $payload;
}
// multiple queries follows and condition
foreach ($queries as $query) {
if (!self::evaluateFilter($query, $payload)) {
return [];
};
}
return $payload;
}
private static function evaluateFilter(Query $query, array $payload): bool
{
$attribute = $query->getAttribute();
$method = $query->getMethod();
$values = $query->getValues();
// during 'and' and 'or' attribute will not be present
switch ($method) {
case Query::TYPE_AND:
// All subqueries must evaluate to true
foreach ($query->getValues() as $subquery) {
if (!self::evaluateFilter($subquery, $payload)) {
return false;
}
}
return true;
case Query::TYPE_OR:
// At least one subquery must evaluate to true
foreach ($query->getValues() as $subquery) {
if (self::evaluateFilter($subquery, $payload)) {
return true;
}
}
return false;
}
$hasAttribute = \array_key_exists($attribute, $payload);
if (!$hasAttribute) {
return false;
}
// null can be a value as well
$payloadAttributeValue = $payload[$attribute];
switch ($method) {
case Query::TYPE_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue === $value);
case Query::TYPE_NOT_EQUAL:
return !self::anyMatch($values, fn ($value) => $payloadAttributeValue === $value);
case Query::TYPE_LESSER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue < $value);
case Query::TYPE_LESSER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue <= $value);
case Query::TYPE_GREATER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue > $value);
case Query::TYPE_GREATER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue >= $value);
// attribute must be present and should be explicitly null
case Query::TYPE_IS_NULL:
return $payloadAttributeValue === null;
case Query::TYPE_IS_NOT_NULL:
return $payloadAttributeValue !== null;
default:
throw new \InvalidArgumentException(
"Unsupported query method: {$method}"
);
}
}
private static function anyMatch(array $values, callable $fn): bool
{
foreach ($values as $value) {
if ($fn($value)) {
return true;
}
}
return false;
}
}
+10 -4
View File
@@ -2428,15 +2428,21 @@ trait Base
case self::GET_DB_HEALTH:
return 'query getDbHealth {
healthGetDB {
ping
status
statuses {
ping
status
}
total
}
}';
case self::GET_CACHE_HEALTH:
return 'query getCacheHealth {
healthGetCache {
ping
status
statuses {
ping
status
}
total
}
}';
case self::GET_TIME_HEALTH:
@@ -51,6 +51,8 @@ class HealthTest extends Scope
$this->assertArrayNotHasKey('errors', $dbHealth['body']);
$dbHealth = $dbHealth['body']['data']['healthGetDB'];
$this->assertIsArray($dbHealth);
$this->assertIsArray($dbHealth['statuses']);
$this->assertGreaterThan(0, $dbHealth['total']);
return $dbHealth;
}
@@ -72,6 +74,8 @@ class HealthTest extends Scope
$this->assertArrayNotHasKey('errors', $cacheHealth['body']);
$cacheHealth = $cacheHealth['body']['data']['healthGetCache'];
$this->assertIsArray($cacheHealth);
$this->assertIsArray($cacheHealth['statuses']);
$this->assertGreaterThan(0, $cacheHealth['total']);
return $cacheHealth;
}
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Health;
class AntiVirusTest extends HealthBase
{
public function testAntiVirus(): void
{
$response = $this->callGet('/health/anti-virus');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['status']);
$this->assertIsString($response['body']['status']);
$this->assertIsString($response['body']['version']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class AuditsQueueTest extends HealthBase
{
public function testAuditsQueue(): void
{
$response = $this->callGet('/health/queue/audits');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/audits', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class BuildsQueueTest extends HealthBase
{
public function testBuildsQueue(): void
{
$response = $this->callGet('/health/queue/builds');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/builds', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\E2E\Services\Health;
class CacheTest extends HealthBase
{
public function testCacheSuccess(): void
{
$response = $this->callGet('/health/cache');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['statuses']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Tests\E2E\Services\Health;
class CertificateTest extends HealthBase
{
public function testCertificateValidity(): void
{
$this->assertCertificate('www.google.com', '/CN=www.google.com', 'www.google.com');
$this->assertCertificate('appwrite.io', '/CN=appwrite.io', 'appwrite.io');
$response = $this->callGet('/health/certificate', ['domain' => 'https://google.com']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCertificateFailure('localhost', 400);
$this->assertCertificateFailure('doesnotexist.com', 404);
$this->assertCertificateFailure('www.google.com/usr/src/local', 400);
$this->assertCertificateFailure('', 400);
}
private function assertCertificate(string $domain, string $expectedName, string $expectedSN): void
{
$response = $this->callGet('/health/certificate', ['domain' => $domain]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($expectedName, $response['body']['name']);
$this->assertEquals($expectedSN, $response['body']['subjectSN']);
$this->assertContains($response['body']['issuerOrganisation'], ["Let's Encrypt", 'Google Trust Services']);
$this->assertIsInt($response['body']['validFrom']);
$this->assertIsInt($response['body']['validTo']);
}
private function assertCertificateFailure(string $domain, int $status): void
{
$response = $this->callGet('/health/certificate', ['domain' => $domain]);
$this->assertEquals($status, $response['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class CertificatesQueueTest extends HealthBase
{
public function testCertificatesQueue(): void
{
$response = $this->callGet('/health/queue/certificates');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/certificates', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\E2E\Services\Health;
class DBTest extends HealthBase
{
public function testDBSuccess(): void
{
$response = $this->callGet('/health/db');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['statuses']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class DatabasesQueueTest extends HealthBase
{
public function testDatabasesQueue(): void
{
$response = $this->callGet('/health/queue/databases', ['name' => 'database_db_main']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/databases', ['name' => 'database_db_main', 'threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class DeletesQueueTest extends HealthBase
{
public function testDeletesQueue(): void
{
$response = $this->callGet('/health/queue/deletes');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/deletes', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class FunctionsQueueTest extends HealthBase
{
public function testFunctionsQueue(): void
{
$response = $this->callGet('/health/queue/functions');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/functions', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Health;
class HTTPTest extends HealthBase
{
public function testHTTPSuccess(): void
{
$response = $this->callGet('/health');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
}
}
+21 -1
View File
@@ -2,6 +2,26 @@
namespace Tests\E2E\Services\Health;
trait HealthBase
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
abstract class HealthBase extends Scope
{
use ProjectCustom;
use SideServer;
protected function getProjectId(): string
{
return $this->getProject()['$id'];
}
protected function callGet(string $path, array $query = []): array
{
return $this->client->call(Client::METHOD_GET, $path, \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProjectId(),
], $this->getHeaders()), $query);
}
}
@@ -1,544 +0,0 @@
<?php
namespace Tests\E2E\Services\Health;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class HealthCustomServerTest extends Scope
{
use HealthBase;
use ProjectCustom;
use SideServer;
public function testHTTPSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
return [];
}
public function testDBSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/db', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
return [];
}
public function testCacheSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/cache', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
return [];
}
public function testPubSubSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/pubsub', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
return [];
}
public function testTimeSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/time', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['remoteTime']);
$this->assertIsInt($response['body']['localTime']);
$this->assertNotEmpty($response['body']['remoteTime']);
$this->assertNotEmpty($response['body']['localTime']);
$this->assertLessThan(10, $response['body']['diff']);
return [];
}
public function testWebhooksSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/webhooks', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/webhooks?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testLogsSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/logs?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testCertificatesSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/certificates', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/certificates?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testFunctionsSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/functions?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testBuildsSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/builds', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/builds?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testDatabasesSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'database_db_main',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'database_db_main',
'threshold' => '0'
]);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testDeletesSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/deletes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/deletes?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testMailsSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/mails', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/mails?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testMessagingSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/messaging', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/messaging?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testMigrationsSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/migrations', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/migrations?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
return [];
}
public function testStorageLocalSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/storage/local', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
return [];
}
public function testStorageSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/storage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
return [];
}
public function testStorageAntiVirusSuccess(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/anti-virus', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['status']);
$this->assertIsString($response['body']['status']);
$this->assertIsString($response['body']['version']);
return [];
}
public function testCertificateValidity(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=www.google.com', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('/CN=www.google.com', $response['body']['name']);
$this->assertEquals('www.google.com', $response['body']['subjectSN']);
$this->assertContains($response['body']['issuerOrganisation'], ['Let\'s Encrypt', 'Google Trust Services']);
$this->assertIsInt($response['body']['validFrom']);
$this->assertIsInt($response['body']['validTo']);
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=appwrite.io', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('/CN=appwrite.io', $response['body']['name']);
$this->assertEquals('appwrite.io', $response['body']['subjectSN']);
$this->assertContains($response['body']['issuerOrganisation'], ['Let\'s Encrypt', 'Google Trust Services']);
$this->assertIsInt($response['body']['validFrom']);
$this->assertIsInt($response['body']['validTo']);
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=https://google.com', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=localhost', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=doesnotexist.com', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=www.google.com/usr/src/local', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(400, $response['headers']['status-code']);
return [];
}
public function testStatsResources()
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-resources', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-resources?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
}
public function testUsageSuccess()
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(503, $response['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class LogsQueueTest extends HealthBase
{
public function testLogsQueue(): void
{
$response = $this->callGet('/health/queue/logs');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/logs', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class MailsQueueTest extends HealthBase
{
public function testMailsQueue(): void
{
$response = $this->callGet('/health/queue/mails');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/mails', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class MessagingQueueTest extends HealthBase
{
public function testMessagingQueue(): void
{
$response = $this->callGet('/health/queue/messaging');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/messaging', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class MigrationsQueueTest extends HealthBase
{
public function testMigrationsQueue(): void
{
$response = $this->callGet('/health/queue/migrations');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/migrations', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\E2E\Services\Health;
class PubSubTest extends HealthBase
{
public function testPubSubSuccess(): void
{
$response = $this->callGet('/health/pubsub');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['statuses']);
$this->assertIsInt($response['body']['statuses'][0]['ping']);
$this->assertLessThan(100, $response['body']['statuses'][0]['ping']);
$this->assertEquals('pass', $response['body']['statuses'][0]['status']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class StatsResourcesQueueTest extends HealthBase
{
public function testStatsResources(): void
{
$response = $this->callGet('/health/queue/stats-resources');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/stats-resources', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class StatsUsageQueueTest extends HealthBase
{
public function testStatsUsage(): void
{
$response = $this->callGet('/health/queue/stats-usage');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/stats-usage', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Health;
class StorageLocalTest extends HealthBase
{
public function testStorageLocal(): void
{
$response = $this->callGet('/health/storage/local');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Health;
class StorageTest extends HealthBase
{
public function testStorage(): void
{
$response = $this->callGet('/health/storage');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('pass', $response['body']['status']);
$this->assertIsInt($response['body']['ping']);
$this->assertLessThan(100, $response['body']['ping']);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class TimeTest extends HealthBase
{
public function testTimeSuccess(): void
{
$response = $this->callGet('/health/time');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['remoteTime']);
$this->assertIsInt($response['body']['localTime']);
$this->assertNotEmpty($response['body']['remoteTime']);
$this->assertNotEmpty($response['body']['localTime']);
$this->assertLessThan(10, $response['body']['diff']);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Tests\E2E\Services\Health;
class WebhooksQueueTest extends HealthBase
{
public function testWebhooksQueue(): void
{
$response = $this->callGet('/health/queue/webhooks');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
$failure = $this->callGet('/health/queue/webhooks', ['threshold' => '0']);
$this->assertEquals(503, $failure['headers']['status-code']);
}
}
+3 -1
View File
@@ -10,7 +10,8 @@ trait RealtimeBase
private function getWebsocket(
array $channels = [],
array $headers = [],
string $projectId = null
string $projectId = null,
array $queries = []
): WebSocketClient {
if (is_null($projectId)) {
$projectId = $this->getProject()['$id'];
@@ -19,6 +20,7 @@ trait RealtimeBase
$query = [
"project" => $projectId,
"channels" => $channels,
"queries" => $queries
];
return new WebSocketClient(
File diff suppressed because it is too large Load Diff
@@ -692,8 +692,8 @@ class RealtimeCustomClientTest extends Scope
$client = $this->getWebsocket(['documents', 'collections'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
]);
'cookie' => 'a_session_' . $projectId . '=' . $session,
], null);
$response = json_decode($client->receive(), true);
@@ -2962,7 +2962,7 @@ class RealtimeCustomClientTest extends Scope
sleep(1);
try {
$client->receive(1); // 1 second timeout
$client->receive();
$this->fail('Should not receive any event after rollback');
} catch (TimeoutException $e) {
// Expected - no event should be triggered
@@ -0,0 +1,602 @@
<?php
namespace Tests\Unit\Utopia\Database\Query;
use Appwrite\Utopia\Database\RuntimeQuery;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Query;
class RuntimeQueryTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testFilterEmptyQueries(): void
{
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([], $payload);
$this->assertEquals($payload, $result);
}
public function testFilterWithNoMatchingQuery(): void
{
$queries = [Query::equal('name', ['Jane'])];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals([], $result);
}
public function testFilterWithMatchingQuery(): void
{
$queries = [Query::equal('name', ['John'])];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals($payload, $result);
}
// TYPE_EQUAL tests
public function testEqualMatch(): void
{
$query = Query::equal('name', ['John']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualNoMatch(): void
{
$query = Query::equal('name', ['Jane']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testEqualMultipleValuesMatch(): void
{
$query = Query::equal('status', ['active', 'pending', 'approved']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualMultipleValuesNoMatch(): void
{
$query = Query::equal('status', ['active', 'pending', 'approved']);
$payload = ['status' => 'rejected'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testEqualNumericValues(): void
{
$query = Query::equal('age', [30, 25, 35]);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualBooleanValues(): void
{
$query = Query::equal('active', [true]);
$payload = ['active' => true];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualMissingAttribute(): void
{
$query = Query::equal('missing', ['value']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
// TYPE_NOT_EQUAL tests
public function testNotEqualMatch(): void
{
$query = Query::notEqual('name', ['Jane']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testNotEqualNoMatch(): void
{
$query = Query::notEqual('name', ['John']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testNotEqualMultipleValues(): void
{
// generally from the client side they will pass query strings via the realtime
// and Query::parse will be done first and parse doesn't allow multiple notEqual values
$query = Query::notEqual('status', ['rejected', 'cancelled']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
$query = Query::notEqual('status', ['active', 'pending']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
// TYPE_LESSER tests
public function testLesserMatch(): void
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testLesserNoMatch(): void
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testLesserEqualValue(): void
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testLesserMultipleValues(): void
{
// Note: Query::lessThan only accepts single value, but RuntimeQuery's anyMatch supports arrays
// This test uses a single value as Query class requires
$query = Query::lessThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testLesserStringComparison(): void
{
$query = Query::lessThan('name', 'M');
$payload = ['name' => 'A'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// TYPE_LESSER_EQUAL tests
public function testLesserEqualMatch(): void
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testLesserEqualExactMatch(): void
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testLesserEqualNoMatch(): void
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testLesserEqualMultipleValues(): void
{
// Note: Query::lessThanEqual only accepts single value
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// TYPE_GREATER tests
public function testGreaterMatch(): void
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testGreaterNoMatch(): void
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testGreaterEqualValue(): void
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testGreaterMultipleValues(): void
{
// Note: Query::greaterThan only accepts single value
$query = Query::greaterThan('age', 20);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// TYPE_GREATER_EQUAL tests
public function testGreaterEqualMatch(): void
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testGreaterEqualExactMatch(): void
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testGreaterEqualNoMatch(): void
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testGreaterEqualMultipleValues(): void
{
// Note: Query::greaterThanEqual only accepts single value
$query = Query::greaterThanEqual('age', 20);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// TYPE_IS_NULL tests
public function testIsNullMatch(): void
{
$query = Query::isNull('description');
$payload = ['description' => null];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testIsNullNoMatch(): void
{
$query = Query::isNull('description');
$payload = ['description' => 'Some text'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testIsNullMissingAttribute(): void
{
$query = Query::isNull('missing');
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
// TYPE_IS_NOT_NULL tests
public function testIsNotNullMatch(): void
{
$query = Query::isNotNull('description');
$payload = ['description' => 'Some text'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testIsNotNullNoMatch(): void
{
$query = Query::isNotNull('description');
$payload = ['description' => null];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testIsNotNullMissingAttribute(): void
{
$query = Query::isNotNull('missing');
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
// TYPE_AND tests
public function testAndAllMatch(): void
{
$query = Query::and([
Query::equal('name', ['John']),
Query::equal('age', [30])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testAndOneFails(): void
{
$query = Query::and([
Query::equal('name', ['John']),
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testAndAllFail(): void
{
$query = Query::and([
Query::equal('name', ['Jane']),
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testAndMultipleConditions(): void
{
$query = Query::and([
Query::equal('status', ['active']),
Query::greaterThan('age', 18),
Query::isNotNull('email')
]);
$payload = ['status' => 'active', 'age' => 25, 'email' => 'test@example.com'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testAndNestedAnd(): void
{
$query = Query::and([
Query::equal('name', ['John']),
Query::and([
Query::equal('age', [30]),
Query::equal('status', ['active'])
])
]);
$payload = ['name' => 'John', 'age' => 30, 'status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// TYPE_OR tests
public function testOrOneMatch(): void
{
$query = Query::or([
Query::equal('name', ['John']),
Query::equal('name', ['Jane'])
]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrAllMatch(): void
{
$query = Query::or([
Query::equal('status', ['active']),
Query::equal('status', ['pending'])
]);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrAllFail(): void
{
$query = Query::or([
Query::equal('name', ['Jane']),
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testOrMultipleConditions(): void
{
$query = Query::or([
Query::equal('status', ['active']),
Query::equal('status', ['pending']),
Query::equal('status', ['approved'])
]);
$payload = ['status' => 'pending'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrNestedOr(): void
{
$query = Query::or([
Query::equal('name', ['John']),
Query::or([
Query::equal('name', ['Jane']),
Query::equal('name', ['Bob'])
])
]);
$payload = ['name' => 'Bob'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrWithDifferentAttributes(): void
{
$query = Query::or([
Query::equal('name', ['John']),
Query::equal('email', ['john@example.com'])
]);
$payload = ['name' => 'Jane', 'email' => 'john@example.com'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// Complex combinations
public function testAndOrCombination(): void
{
$query = Query::and([
Query::equal('type', ['user']),
Query::or([
Query::equal('status', ['active']),
Query::equal('status', ['pending'])
])
]);
$payload = ['type' => 'user', 'status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrAndCombination(): void
{
$query = Query::or([
Query::and([
Query::equal('name', ['John']),
Query::equal('age', [30])
]),
Query::and([
Query::equal('name', ['Jane']),
Query::equal('age', [25])
])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
// Edge cases
public function testMultipleQueriesAllMatch(): void
{
$queries = [
Query::equal('name', ['John']),
Query::equal('age', [30])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals($payload, $result);
}
public function testMultipleQueriesFirstMatches(): void
{
$queries = [
Query::equal('name', ['John']),
Query::equal('age', [25])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
// With AND logic, if first matches but second doesn't, should return empty
$this->assertEquals([], $result);
}
public function testMultipleQueriesSecondMatches(): void
{
$queries = [
Query::equal('name', ['Jane']),
Query::equal('age', [30])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
// With AND logic, if second matches but first doesn't, should return empty
$this->assertEquals([], $result);
}
public function testMultipleQueriesNoneMatch(): void
{
$queries = [
Query::equal('name', ['Jane']),
Query::equal('age', [25])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals([], $result);
}
public function testEmptyPayload(): void
{
$query = Query::equal('name', ['John']);
$payload = [];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
}
public function testEmptyAndQuery(): void
{
$query = Query::and([]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
// Empty AND should return true (all conditions pass vacuously)
$this->assertEquals($payload, $result);
}
public function testEmptyOrQuery(): void
{
$query = Query::or([]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
// Empty OR should return false (no conditions match)
$this->assertEquals([], $result);
}
// Type-specific edge cases
public function testEqualWithZero(): void
{
$query = Query::equal('count', [0]);
$payload = ['count' => 0];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualWithEmptyString(): void
{
$query = Query::equal('name', ['']);
$payload = ['name' => ''];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testEqualWithFalse(): void
{
$query = Query::equal('active', [false]);
$payload = ['active' => false];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testComparisonWithFloat(): void
{
$query = Query::greaterThan('score', 8.5);
$payload = ['score' => 9.2];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testComparisonWithStringNumbers(): void
{
$query = Query::lessThan('version', '10');
$payload = ['version' => '9'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals($payload, $result);
}
}