mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
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:
@@ -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=
|
||||
|
||||
@@ -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' => [
|
||||
[
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user