From 4fd0e9e02deceb92cd46e119b4041347fb6307c5 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 Mar 2024 13:10:25 +0000 Subject: [PATCH 001/241] supporting mime types fo avif and heic images --- app/config/storage/mimes.php | 2 ++ app/config/storage/outputs.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/config/storage/mimes.php b/app/config/storage/mimes.php index 5d315f45bc..a9eaec59d3 100644 --- a/app/config/storage/mimes.php +++ b/app/config/storage/mimes.php @@ -6,6 +6,8 @@ return [ 'image/gif', 'image/png', 'image/webp', + 'image/heic', + 'image/avif', // Video Files 'video/mp4', diff --git a/app/config/storage/outputs.php b/app/config/storage/outputs.php index 507a9ce667..8de4f18599 100644 --- a/app/config/storage/outputs.php +++ b/app/config/storage/outputs.php @@ -6,4 +6,7 @@ return [ // Accepted outputs files 'gif' => 'image/gif', 'png' => 'image/png', 'webp' => 'image/webp', + 'heic' => 'image/heic', + 'heics' => 'image/heic', + 'avif' => 'image/avif' ]; From 06a5e76c5047a8fe941cd9cc48d29bad5d482fe5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 23 May 2024 17:42:03 +0300 Subject: [PATCH 002/241] ScheduleBase refactor --- app/init.php | 4 ++-- src/Appwrite/Platform/Tasks/ScheduleBase.php | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/init.php b/app/init.php index 57ad5fab87..45c28319f4 100644 --- a/app/init.php +++ b/app/init.php @@ -1454,9 +1454,9 @@ App::setResource('deviceForBuilds', function ($project) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); }, ['project']); -function getDevice($root): Device +function getDevice(string $root, string $connection = null): Device { - $connection = System::getEnv('_APP_CONNECTIONS_STORAGE', ''); + $connection = empty($connection) ? System::getEnv('_APP_CONNECTIONS_STORAGE', '') : System::getEnv($connection, ''); if (!empty($connection)) { $acl = 'private'; diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index a50fbb2403..c8c183603e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -27,7 +27,8 @@ abstract class ScheduleBase extends Action abstract protected function enqueueResources( Group $pools, - Database $dbForConsole + Database $dbForConsole, + callable $getProjectDB ); public function __construct() @@ -130,7 +131,7 @@ abstract class ScheduleBase extends Action Console::success("Starting timers at " . DateTime::now()); - run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { + run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) { /** * The timer synchronize $schedules copy with database collection. */ @@ -190,10 +191,10 @@ abstract class ScheduleBase extends Action Timer::tick( static::ENQUEUE_TIMER * 1000, - fn () => $this->enqueueResources($pools, $dbForConsole) + fn () => $this->enqueueResources($pools, $dbForConsole, $getProjectDB) ); - $this->enqueueResources($pools, $dbForConsole); + $this->enqueueResources($pools, $dbForConsole, $getProjectDB); }); } } From ecfc5d68c20ed442d9af702242455f79b13f713c Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 23 May 2024 17:49:41 +0300 Subject: [PATCH 003/241] accept null --- app/init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init.php b/app/init.php index 45c28319f4..0f9b3b3bfa 100644 --- a/app/init.php +++ b/app/init.php @@ -1454,7 +1454,7 @@ App::setResource('deviceForBuilds', function ($project) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); }, ['project']); -function getDevice(string $root, string $connection = null): Device +function getDevice(string $root, ?string $connection = null): Device { $connection = empty($connection) ? System::getEnv('_APP_CONNECTIONS_STORAGE', '') : System::getEnv($connection, ''); From 0994b0f2dee1dcf940191b0e81a95c265d89eba4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 23 May 2024 18:14:00 +0300 Subject: [PATCH 004/241] trigger migration --- src/Appwrite/Event/Migration.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index 478291829b..1bba6e6180 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -10,6 +10,7 @@ class Migration extends Event { protected string $type = ''; protected ?Document $migration = null; + protected ?Document $backup; public function __construct(protected Connection $connection) { @@ -33,6 +34,19 @@ class Migration extends Event return $this; } + /** + * Sets backup. + * + * @param Document $backup + * @return self + */ + public function setBackup(Document $backup): self + { + $this->backup = $backup; + + return $this; + } + /** * Returns set migration document for the function event. * @@ -81,7 +95,8 @@ class Migration extends Event return $client->enqueue([ 'project' => $this->project, 'user' => $this->user, - 'migration' => $this->migration + 'migration' => $this->migration, + 'backup' => $this->backup ]); } } From 552c729c5a50d7ab94302b0ccd5ee09cea54c326 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 23 May 2024 18:22:25 +0300 Subject: [PATCH 005/241] fix enqueueResources --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 7 +------ src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 2 +- src/Appwrite/Platform/Tasks/ScheduleMessages.php | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index c8c183603e..605082b2d3 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -24,12 +24,7 @@ abstract class ScheduleBase extends Action abstract public static function getName(): string; abstract public static function getSupportedResource(): string; - - abstract protected function enqueueResources( - Group $pools, - Database $dbForConsole, - callable $getProjectDB - ); + abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB):void; public function __construct() { diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index e2c278714f..98a935876a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -26,7 +26,7 @@ class ScheduleFunctions extends ScheduleBase return 'function'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { $timerStart = \microtime(true); $time = DateTime::now(); diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 8e52973a0c..ac2eaa3302 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -21,7 +21,7 @@ class ScheduleMessages extends ScheduleBase return 'message'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { foreach ($this->schedules as $schedule) { if (!$schedule['active']) { From d4b0ca7804ced1c72c98857bc0ee99660e686f41 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 26 May 2024 11:05:18 +0300 Subject: [PATCH 006/241] add scopes --- tests/e2e/Scopes/ProjectCustom.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index ad2c93790c..3edc35a34c 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -94,6 +94,8 @@ trait ProjectCustom 'topics.read', 'subscribers.write', 'subscribers.read', + 'backups.write', + 'backups.read' ], ]); From a7b7059916ed377bf69c773d3c4a8ff363f0897f Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 26 May 2024 11:37:16 +0300 Subject: [PATCH 007/241] revert scope --- tests/e2e/Scopes/ProjectCustom.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 3edc35a34c..ad2c93790c 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -94,8 +94,6 @@ trait ProjectCustom 'topics.read', 'subscribers.write', 'subscribers.read', - 'backups.write', - 'backups.read' ], ]); From bbcab38590e91f9de8877bcf2aa5c73795ec416a Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 26 May 2024 12:14:40 +0300 Subject: [PATCH 008/241] add config backups scopes --- app/config/scopes.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/config/scopes.php b/app/config/scopes.php index 3765ab54fa..7243e35f96 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -130,4 +130,10 @@ return [ // List of publicly visible scopes 'assistant.read' => [ 'description' => 'Access to read the Assistant service', ], + 'backups.write' => [ + 'description' => 'Access to create, update, and delete your backups', + ], + 'backups.read' => [ + 'description' => 'Access to read the backups service', + ], ]; From 9c591bd42d5b9008cb3678b145cfd2e8964f0643 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 17:06:08 +0300 Subject: [PATCH 009/241] add resourceType backup --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 605082b2d3..4b6a27e53f 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -60,7 +60,8 @@ abstract class ScheduleBase extends Action $collectionId = match ($schedule->getAttribute('resourceType')) { 'function' => 'functions', - 'message' => 'messages' + 'message' => 'messages', + 'backup' => 'backupsPolicy' }; $resource = $getProjectDB($project)->getDocument( From b5061da8c3cd7dc09251229819acdc483cae521e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 17:17:58 +0300 Subject: [PATCH 010/241] add resourceType backup --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 4b6a27e53f..313730e9c2 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -110,7 +110,8 @@ abstract class ScheduleBase extends Action } catch (\Throwable $th) { $collectionId = match ($document->getAttribute('resourceType')) { 'function' => 'functions', - 'message' => 'messages' + 'message' => 'messages', + 'backup' => 'backupsPolicy' }; Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}"); From bc62405810fe74abe52f880ef6e218767e214e46 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 17:42:51 +0300 Subject: [PATCH 011/241] Add debug --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 313730e9c2..ec0022dc79 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -106,6 +106,9 @@ abstract class ScheduleBase extends Action foreach ($results as $document) { try { + var_dump('=== ScheduleBase start'); + var_dump($getSchedule($document)); + var_dump('=== ScheduleBase end'); $this->schedules[$document['resourceId']] = $getSchedule($document); } catch (\Throwable $th) { $collectionId = match ($document->getAttribute('resourceType')) { From f68f21a26419d9568167f0e86d9eb3f0cf277bde Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 17:53:50 +0300 Subject: [PATCH 012/241] Add debug --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index ec0022dc79..c155f6069c 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -107,7 +107,7 @@ abstract class ScheduleBase extends Action foreach ($results as $document) { try { var_dump('=== ScheduleBase start'); - var_dump($getSchedule($document)); + var_dump($getSchedule($document)['resource']); var_dump('=== ScheduleBase end'); $this->schedules[$document['resourceId']] = $getSchedule($document); } catch (\Throwable $th) { From ddb0ce317533dea708fb32174cccbccadc3d2f15 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 18:52:33 +0300 Subject: [PATCH 013/241] add destinations --- app/config/collections.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 72d126e343..73dbc9eaa5 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4040,6 +4040,17 @@ $projectCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('destination'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 500, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('credentials'), 'type' => Database::VAR_STRING, From c08d34ead9fd0387d873f2ac594792bd519403fb Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 27 May 2024 18:56:28 +0300 Subject: [PATCH 014/241] add destinations to api's --- app/controllers/api/migrations.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 3899b26ad4..dd65d7600b 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -60,6 +60,7 @@ App::post('/v1/migrations/appwrite') 'status' => 'pending', 'stage' => 'init', 'source' => Appwrite::getName(), + 'destination' => Appwrite::getName(), 'credentials' => [ 'endpoint' => $endpoint, 'projectId' => $projectId, @@ -164,6 +165,7 @@ App::post('/v1/migrations/firebase/oauth') 'status' => 'pending', 'stage' => 'init', 'source' => Firebase::getName(), + 'destination' => Appwrite::getName(), 'credentials' => [ 'serviceAccount' => json_encode($serviceAccount), ], @@ -224,6 +226,7 @@ App::post('/v1/migrations/firebase') 'status' => 'pending', 'stage' => 'init', 'source' => Firebase::getName(), + 'destination' => Appwrite::getName(), 'credentials' => [ 'serviceAccount' => $serviceAccount, ], @@ -279,6 +282,7 @@ App::post('/v1/migrations/supabase') 'status' => 'pending', 'stage' => 'init', 'source' => Supabase::getName(), + 'destination' => Appwrite::getName(), 'credentials' => [ 'endpoint' => $endpoint, 'apiKey' => $apiKey, @@ -340,6 +344,7 @@ App::post('/v1/migrations/nhost') 'status' => 'pending', 'stage' => 'init', 'source' => NHost::getName(), + 'destination' => Appwrite::getName(), 'credentials' => [ 'subdomain' => $subdomain, 'region' => $region, From bdb73509947e4ab635e191b399c01c45bfc64d3f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 19:50:37 +0300 Subject: [PATCH 015/241] change private to protected --- src/Appwrite/Platform/Workers/Migrations.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 8e4aa5216d..0b20607637 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -30,8 +30,8 @@ use Utopia\Queue\Message; class Migrations extends Action { - private ?Database $dbForProject = null; - private ?Database $dbForConsole = null; + protected ?Database $dbForProject = null; + protected ?Database $dbForConsole = null; public static function getName(): string { @@ -93,13 +93,15 @@ class Migrations extends Action } /** - * @param string $source - * @param array $credentials + * @param Document $document * @return Source * @throws Exception */ - protected function processSource(string $source, array $credentials): Source + protected function processSource(Document $document): Source { + $source = $document->getAttribute('source'); + $credentials = $document->getAttribute('credentials'); + return match ($source) { Firebase::getName() => new Firebase( json_decode($credentials['serviceAccount'], true), @@ -263,7 +265,8 @@ class Migrations extends Action $log->addTag('type', $migrationDocument->getAttribute('source')); - $source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials')); + //$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials')); + $source = $this->processSource($migrationDocument); $source->report(); From 6149f4662114c7b168a9ef3bf8aea06a6a9df3fe Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 19:59:54 +0300 Subject: [PATCH 016/241] processDestination --- src/Appwrite/Platform/Workers/Migrations.php | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 0b20607637..870cf78331 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -6,6 +6,7 @@ use Appwrite\Event\Event; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Permission; use Appwrite\Role; +use Appwrite\Utopia\Migration\Destinations\Backup; use Exception; use Utopia\CLI\Console; use Utopia\Database\Database; @@ -17,7 +18,8 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Logger\Log; use Utopia\Logger\Log\Breadcrumb; -use Utopia\Migration\Destinations\Appwrite as DestinationsAppwrite; +use Utopia\Migration\Destination; +use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite; @@ -129,6 +131,24 @@ class Migrations extends Action }; } + /** + * @param string $destination + * @param array $credentials + * @return Destination + * @throws Exception + */ + protected function processDestination(string $destination, array $credentials): Destination + { + return match ($destination) { + DestinationAppwrite::getName() => new DestinationAppwrite( + $credentials['projectId'], + str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], + $credentials['apiKey'] + ), + default => throw new \Exception('Invalid destination type'), + }; + } + /** * @throws Authorization * @throws Structure From 73280fd173e312e38beac68fd44cc9733d2cbcf7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 20:05:36 +0300 Subject: [PATCH 017/241] processSource --- src/Appwrite/Platform/Workers/Migrations.php | 29 ++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 870cf78331..da33970bc5 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -95,15 +95,13 @@ class Migrations extends Action } /** - * @param Document $document + * @param string $source + * @param array $credentials * @return Source * @throws Exception */ - protected function processSource(Document $document): Source + protected function processSource(string $source, array $credentials): Source { - $source = $document->getAttribute('source'); - $credentials = $document->getAttribute('credentials'); - return match ($source) { Firebase::getName() => new Firebase( json_decode($credentials['serviceAccount'], true), @@ -126,7 +124,11 @@ class Migrations extends Action $credentials['password'], $credentials['port'], ), - Appwrite::getName() => new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']), + Appwrite::getName() => new Appwrite( + $credentials['projectId'], + str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], + $credentials['apiKey'] + ), default => throw new \Exception('Invalid source type'), }; } @@ -285,8 +287,19 @@ class Migrations extends Action $log->addTag('type', $migrationDocument->getAttribute('source')); - //$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials')); - $source = $this->processSource($migrationDocument); + $source = $this->processSource( + $migrationDocument->getAttribute('source'), + $migrationDocument->getAttribute('credentials') + ); + + $destination = $this->processDestination( + $migrationDocument->getAttribute('destination'), + [ + 'projectId' => $projectDocument->getId(), + 'endpoint' => 'http://appwrite/v1', + 'apiKey' => $tempAPIKey['secret'] + ] + ); $source->report(); From 75ec86eed487de5224acdfa6b6e273a9cac05fda Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 20:12:58 +0300 Subject: [PATCH 018/241] SourceAppwrite --- src/Appwrite/Platform/Workers/Migrations.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index da33970bc5..13f53277ee 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -20,6 +20,7 @@ use Utopia\Logger\Log; use Utopia\Logger\Log\Breadcrumb; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; +use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite; @@ -124,7 +125,7 @@ class Migrations extends Action $credentials['password'], $credentials['port'], ), - Appwrite::getName() => new Appwrite( + SourceAppwrite::getName() => new SourceAppwrite( $credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey'] @@ -303,12 +304,6 @@ class Migrations extends Action $source->report(); - $destination = new DestinationsAppwrite( - $projectDocument->getId(), - 'http://appwrite/v1', - $tempAPIKey['secret'], - ); - $transfer = new Transfer( $source, $destination From d503a436d02a99ea123c7c4adcdf42664b6c90b7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 20:37:37 +0300 Subject: [PATCH 019/241] add project --- src/Appwrite/Platform/Workers/Migrations.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 13f53277ee..dc8db4d626 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -33,8 +33,9 @@ use Utopia\Queue\Message; class Migrations extends Action { - protected ?Database $dbForProject = null; - protected ?Database $dbForConsole = null; + protected Database $dbForProject; + protected Database $dbForConsole; + protected Document $project; public static function getName(): string { @@ -81,6 +82,7 @@ class Migrations extends Action $this->dbForProject = $dbForProject; $this->dbForConsole = $dbForConsole; + $this->project = $project; /** * Handle Event execution. @@ -92,7 +94,7 @@ class Migrations extends Action $log->addTag('migrationId', $migration->getId()); $log->addTag('projectId', $project->getId()); - $this->processMigration($project, $migration, $log); + $this->processMigration($migration, $log); } /** @@ -268,14 +270,9 @@ class Migrations extends Action * @throws Structure * @throws \Utopia\Database\Exception */ - protected function processMigration(Document $project, Document $migration, Log $log): void + protected function processMigration(Document $migration, Log $log): void { - /** - * @var Document $migrationDocument - * @var Transfer $transfer - */ - $migrationDocument = null; - $transfer = null; + $project = $this->project; $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); $tempAPIKey = $this->generateAPIKey($projectDocument); From 68f5947b40a08219dbb97dfffbbfbb24b6351eb6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 20:40:57 +0300 Subject: [PATCH 020/241] Remove backup --- src/Appwrite/Platform/Workers/Migrations.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index dc8db4d626..db2b778834 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -6,7 +6,6 @@ use Appwrite\Event\Event; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Permission; use Appwrite\Role; -use Appwrite\Utopia\Migration\Destinations\Backup; use Exception; use Utopia\CLI\Console; use Utopia\Database\Database; @@ -23,7 +22,6 @@ use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; -use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\Firebase; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; From 15ffce36de414d5fc70bfceb1b3dc455333ed3d3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 28 May 2024 20:52:32 +0300 Subject: [PATCH 021/241] add hooks --- src/Appwrite/Platform/Workers/Migrations.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index db2b778834..4782de001e 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -308,6 +308,9 @@ class Migrations extends Action $migrationDocument->setAttribute('stage', 'migrating'); $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migrationDocument, $projectDocument); + + $destination->init(); + $transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) { $migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache())); $migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); @@ -315,6 +318,8 @@ class Migrations extends Action $this->updateMigrationDocument($migrationDocument, $projectDocument); }); + $destination->shutDown(); + $sourceErrors = $source->getErrors(); $destinationErrors = $destination->getErrors(); From 3ebbc5976fe85836ea32cc6778ba6da3578d909c Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 11:31:00 +0300 Subject: [PATCH 022/241] hint update --- src/Appwrite/Platform/Workers/Migrations.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 4782de001e..3d955cc757 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -258,7 +258,6 @@ class Migrations extends Action } /** - * @param Document $project * @param Document $migration * @param Log $log * @return void From 97703b03320680f4b0206071884332dfdcef0a42 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 18:16:29 +0300 Subject: [PATCH 023/241] Question --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index c155f6069c..5dbc52f886 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -153,6 +153,7 @@ abstract class ScheduleBase extends Action $paginationQueries[] = Query::cursorAfter($latestDocument); } + // do we need add query active = 1? $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), Query::equal('resourceType', [static::getSupportedResource()]), From 07bfe3823c6f6033f3d4f7cc44064a20dd2c6df3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 18:59:10 +0300 Subject: [PATCH 024/241] getCollectionId --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 1 + src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 5 +++++ src/Appwrite/Platform/Tasks/ScheduleMessages.php | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 5dbc52f886..af7fe9d221 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -24,6 +24,7 @@ abstract class ScheduleBase extends Action abstract public static function getName(): string; abstract public static function getSupportedResource(): string; + abstract public static function getCollectionId(): string; abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB):void; public function __construct() diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 98a935876a..4d57902330 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -26,6 +26,11 @@ class ScheduleFunctions extends ScheduleBase return 'function'; } + public static function getCollectionId(): string + { + return 'functions'; + } + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { $timerStart = \microtime(true); diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index ac2eaa3302..5629979dce 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -21,6 +21,11 @@ class ScheduleMessages extends ScheduleBase return 'message'; } + public static function getCollectionId(): string + { + return 'messages'; + } + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { foreach ($this->schedules as $schedule) { From 5764ea9ade2691302ce2cee4340f74fff3cd3f98 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 19:09:14 +0300 Subject: [PATCH 025/241] static getCollectionId --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index af7fe9d221..e06873e825 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,14 +59,8 @@ abstract class ScheduleBase extends Action $getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array { $project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId')); - $collectionId = match ($schedule->getAttribute('resourceType')) { - 'function' => 'functions', - 'message' => 'messages', - 'backup' => 'backupsPolicy' - }; - $resource = $getProjectDB($project)->getDocument( - $collectionId, + static::getCollectionId(), $schedule->getAttribute('resourceId') ); @@ -112,12 +106,7 @@ abstract class ScheduleBase extends Action var_dump('=== ScheduleBase end'); $this->schedules[$document['resourceId']] = $getSchedule($document); } catch (\Throwable $th) { - $collectionId = match ($document->getAttribute('resourceType')) { - 'function' => 'functions', - 'message' => 'messages', - 'backup' => 'backupsPolicy' - }; - + $collectionId = static::getCollectionId(); Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}"); Console::error($th->getMessage()); } From ed0995fa49d57e04c5d4c2f31ba0316c8bd0c0fb Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 19:26:44 +0300 Subject: [PATCH 026/241] Fix bug? --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index e06873e825..2e1e673bf4 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -143,7 +143,6 @@ abstract class ScheduleBase extends Action $paginationQueries[] = Query::cursorAfter($latestDocument); } - // do we need add query active = 1? $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), Query::equal('resourceType', [static::getSupportedResource()]), @@ -154,7 +153,8 @@ abstract class ScheduleBase extends Action $total = $total + $sum; foreach ($results as $document) { - $localDocument = $schedules[$document['resourceId']] ?? null; + // todo: change this to Internal id or add projectId as + $localDocument = $this->schedules[$document['resourceId']] ?? null; // Check if resource has been updated since last sync $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; From d000c160b65730d9de3837fd2811e00376b35c23 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 19:29:03 +0300 Subject: [PATCH 027/241] Fix bug? --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 2e1e673bf4..1c8eb8f7db 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -153,7 +153,7 @@ abstract class ScheduleBase extends Action $total = $total + $sum; foreach ($results as $document) { - // todo: change this to Internal id or add projectId as + // todo: change resourceId to Internal id or add projectId as second nested key $localDocument = $this->schedules[$document['resourceId']] ?? null; // Check if resource has been updated since last sync From 26c714da481258759e1431ec803880918e1e71fb Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 29 May 2024 19:30:52 +0300 Subject: [PATCH 028/241] dbg --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 1c8eb8f7db..ab37849faf 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,6 +59,10 @@ abstract class ScheduleBase extends Action $getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array { $project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId')); + var_dump('===== $getSchedule getCollectionId = '); + var_dump(static::getCollectionId()); + var_dump('===== $getSchedule getCollectionId = '); + $resource = $getProjectDB($project)->getDocument( static::getCollectionId(), $schedule->getAttribute('resourceId') From abde5393df9d88f437dc7decf57813f5a2dc016b Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 16:18:40 +0300 Subject: [PATCH 029/241] Set Archive --- src/Appwrite/Event/Migration.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index 1bba6e6180..29ce2c2716 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -10,7 +10,8 @@ class Migration extends Event { protected string $type = ''; protected ?Document $migration = null; - protected ?Document $backup; + protected ?Document $archive = null; + protected ?Document $backup = null; public function __construct(protected Connection $connection) { @@ -35,14 +36,14 @@ class Migration extends Event } /** - * Sets backup. + * Sets archive * - * @param Document $backup + * @param Document $archive * @return self */ - public function setBackup(Document $backup): self + public function setArchive(Document $archive): self { - $this->backup = $backup; + $this->archive = $archive; return $this; } @@ -96,7 +97,7 @@ class Migration extends Event 'project' => $this->project, 'user' => $this->user, 'migration' => $this->migration, - 'backup' => $this->backup + 'archive' => $this->archive ]); } } From 0e33fb2b64bb845f26d6dfe53d6a9c7699a8df98 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 16:19:02 +0300 Subject: [PATCH 030/241] remove $backup --- src/Appwrite/Event/Migration.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index 29ce2c2716..c5af751a6b 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -11,7 +11,6 @@ class Migration extends Event protected string $type = ''; protected ?Document $migration = null; protected ?Document $archive = null; - protected ?Document $backup = null; public function __construct(protected Connection $connection) { From d5025e7db37f350efdd95b08efacf328d9bde871 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 16:59:23 +0300 Subject: [PATCH 031/241] add todo --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index ab37849faf..392d232f8b 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -108,6 +108,7 @@ abstract class ScheduleBase extends Action var_dump('=== ScheduleBase start'); var_dump($getSchedule($document)['resource']); var_dump('=== ScheduleBase end'); + //todo: use a unique key as InternalId or add projectId $this->schedules[$document['resourceId']] = $getSchedule($document); } catch (\Throwable $th) { $collectionId = static::getCollectionId(); From 25b661b908ed3e05b320e4cf2f0346b0bee712e1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 17:15:17 +0300 Subject: [PATCH 032/241] Move Archive event to cloud --- src/Appwrite/Event/Migration.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index c5af751a6b..e57ac3c87c 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -10,7 +10,6 @@ class Migration extends Event { protected string $type = ''; protected ?Document $migration = null; - protected ?Document $archive = null; public function __construct(protected Connection $connection) { @@ -34,19 +33,6 @@ class Migration extends Event return $this; } - /** - * Sets archive - * - * @param Document $archive - * @return self - */ - public function setArchive(Document $archive): self - { - $this->archive = $archive; - - return $this; - } - /** * Returns set migration document for the function event. * @@ -96,7 +82,6 @@ class Migration extends Event 'project' => $this->project, 'user' => $this->user, 'migration' => $this->migration, - 'archive' => $this->archive ]); } } From b23f7b0725824d9fcce1f7ccce77deef6c3da868 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 19:25:49 +0300 Subject: [PATCH 033/241] Revert archive --- src/Appwrite/Event/Migration.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index e57ac3c87c..c33e8ec767 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -10,6 +10,7 @@ class Migration extends Event { protected string $type = ''; protected ?Document $migration = null; + protected ?Document $archive = null; public function __construct(protected Connection $connection) { @@ -82,6 +83,20 @@ class Migration extends Event 'project' => $this->project, 'user' => $this->user, 'migration' => $this->migration, + 'archive' => $this->archive ]); } + + /** + * Sets archive + * + * @param Document $archive + * @return self + */ + public function setArchive(Document $archive): self + { + $this->archive = $archive; + + return $this; + } } From 91cc3a3ff5478bf0c239fae925b171fea2024884 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 9 Jun 2024 19:36:41 +0300 Subject: [PATCH 034/241] Remove archive --- src/Appwrite/Event/Migration.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index c33e8ec767..e57ac3c87c 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -10,7 +10,6 @@ class Migration extends Event { protected string $type = ''; protected ?Document $migration = null; - protected ?Document $archive = null; public function __construct(protected Connection $connection) { @@ -83,20 +82,6 @@ class Migration extends Event 'project' => $this->project, 'user' => $this->user, 'migration' => $this->migration, - 'archive' => $this->archive ]); } - - /** - * Sets archive - * - * @param Document $archive - * @return self - */ - public function setArchive(Document $archive): self - { - $this->archive = $archive; - - return $this; - } } From 82be39af3d08778cd3787df418abd25e834b3365 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 13 Jun 2024 15:14:56 +0300 Subject: [PATCH 035/241] Update cache --- composer.json | 2 +- composer.lock | 105 +++++++++++++++++++++++++------------------------- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 205fe308f0..c847612f65 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "utopia-php/abuse": "0.37.*", "utopia-php/analytics": "0.10.*", "utopia-php/audit": "0.39.*", - "utopia-php/cache": "0.9.*", + "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.49.*", diff --git a/composer.lock b/composer.lock index bfe8fc0730..64b9f59d21 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "53996479cd4ba0c73dbc72d46b240be0", + "content-hash": "44dc3f42d5df8bab8d3e45ff631f384d", "packages": [ { "name": "adhocore/jwt", @@ -1427,16 +1427,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.37.0", + "version": "0.37.1", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "2de5c12886cbd516e511e559afdd9e615d871062" + "reference": "4dfcff4754c7804d1a70039792c0f2d59a5cc981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/2de5c12886cbd516e511e559afdd9e615d871062", - "reference": "2de5c12886cbd516e511e559afdd9e615d871062", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/4dfcff4754c7804d1a70039792c0f2d59a5cc981", + "reference": "4dfcff4754c7804d1a70039792c0f2d59a5cc981", "shasum": "" }, "require": { @@ -1470,9 +1470,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.37.0" + "source": "https://github.com/utopia-php/abuse/tree/0.37.1" }, - "time": "2024-03-06T21:20:27+00:00" + "time": "2024-06-05T18:03:59+00:00" }, { "name": "utopia-php/analytics", @@ -1522,16 +1522,16 @@ }, { "name": "utopia-php/audit", - "version": "0.39.0", + "version": "0.39.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "f0bc15012e05cc0b9dde012ab27d25f193768a2c" + "reference": "7ea91e0ceea7b94293612fea94022b73315677c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/f0bc15012e05cc0b9dde012ab27d25f193768a2c", - "reference": "f0bc15012e05cc0b9dde012ab27d25f193768a2c", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/7ea91e0ceea7b94293612fea94022b73315677c2", + "reference": "7ea91e0ceea7b94293612fea94022b73315677c2", "shasum": "" }, "require": { @@ -1563,22 +1563,22 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.39.0" + "source": "https://github.com/utopia-php/audit/tree/0.39.1" }, - "time": "2024-03-06T21:20:37+00:00" + "time": "2024-06-05T19:28:22+00:00" }, { "name": "utopia-php/cache", - "version": "0.9.1", + "version": "0.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6" + "reference": "313bcdfbb166f75c2c205a59d1467cead63a9626" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/552b4c554bb14d0c529631ce304cdf4a2b9d06a6", - "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/313bcdfbb166f75c2c205a59d1467cead63a9626", + "reference": "313bcdfbb166f75c2c205a59d1467cead63a9626", "shasum": "" }, "require": { @@ -1613,9 +1613,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.9.1" + "source": "https://github.com/utopia-php/cache/tree/0.10.0" }, - "time": "2024-03-19T17:07:20+00:00" + "time": "2024-06-05T16:40:43+00:00" }, { "name": "utopia-php/cli", @@ -1719,23 +1719,23 @@ }, { "name": "utopia-php/database", - "version": "0.49.10", + "version": "0.49.12", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "216209121bc97a2010f67a39c561fafe1e936bec" + "reference": "45def2f7c6bc5f631dbb67e5df0e8e7331af5f63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/216209121bc97a2010f67a39c561fafe1e936bec", - "reference": "216209121bc97a2010f67a39c561fafe1e936bec", + "url": "https://api.github.com/repos/utopia-php/database/zipball/45def2f7c6bc5f631dbb67e5df0e8e7331af5f63", + "reference": "45def2f7c6bc5f631dbb67e5df0e8e7331af5f63", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-pdo": "*", "php": ">=8.0", - "utopia-php/cache": "0.9.*", + "utopia-php/cache": "0.10.*", "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, @@ -1769,9 +1769,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.49.10" + "source": "https://github.com/utopia-php/database/tree/0.49.12" }, - "time": "2024-05-20T02:14:20+00:00" + "time": "2024-06-05T16:52:59+00:00" }, { "name": "utopia-php/domains", @@ -2755,22 +2755,22 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.6", + "version": "0.6.7", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "e538264cfee5e3efdfe1771efba04750cf20b2c4" + "reference": "8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/e538264cfee5e3efdfe1771efba04750cf20b2c4", - "reference": "e538264cfee5e3efdfe1771efba04750cf20b2c4", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974", + "reference": "8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "^0.9.0", + "utopia-php/cache": "^0.10.0", "utopia-php/framework": "0.*.*" }, "require-dev": { @@ -2798,9 +2798,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.6" + "source": "https://github.com/utopia-php/vcs/tree/0.6.7" }, - "time": "2024-05-17T09:36:30+00:00" + "time": "2024-06-05T17:38:29+00:00" }, { "name": "utopia-php/websocket", @@ -2987,16 +2987,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.38.6", + "version": "0.38.7", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "d7016d6d72545e84709892faca972eb4bf5bd699" + "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d7016d6d72545e84709892faca972eb4bf5bd699", - "reference": "d7016d6d72545e84709892faca972eb4bf5bd699", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", + "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", "shasum": "" }, "require": { @@ -3032,9 +3032,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.38.6" + "source": "https://github.com/appwrite/sdk-generator/tree/0.38.7" }, - "time": "2024-05-20T18:00:16+00:00" + "time": "2024-06-10T00:23:02+00:00" }, { "name": "doctrine/deprecations", @@ -3345,16 +3345,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -3362,11 +3362,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -3392,7 +3393,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -3400,7 +3401,7 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", @@ -3824,16 +3825,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -3865,9 +3866,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-05-06T12:04:23+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "phpunit/php-code-coverage", From 2c80823d96304dfe86e1a6c6703905404fde27e2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 13 Jun 2024 18:26:13 +0300 Subject: [PATCH 036/241] composer.lock --- composer.lock | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index aee6c86d53..b9c45b149f 100644 --- a/composer.lock +++ b/composer.lock @@ -2988,16 +2988,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.38.6", + "version": "0.38.7", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "d7016d6d72545e84709892faca972eb4bf5bd699" + "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d7016d6d72545e84709892faca972eb4bf5bd699", - "reference": "d7016d6d72545e84709892faca972eb4bf5bd699", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", + "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", "shasum": "" }, "require": { @@ -3033,9 +3033,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.38.6" + "source": "https://github.com/appwrite/sdk-generator/tree/0.38.7" }, - "time": "2024-05-20T18:00:16+00:00" + "time": "2024-06-10T00:23:02+00:00" }, { "name": "doctrine/deprecations", @@ -3346,16 +3346,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -3363,11 +3363,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -3393,7 +3394,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -3401,7 +3402,7 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", From e58033aff0932266be363ad473541398d8a04e69 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 13 Jun 2024 19:03:51 +0300 Subject: [PATCH 037/241] Downgrade utopia-php/platform to 0.5 --- composer.json | 2 +- composer.lock | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 192b311822..9ba8db0e09 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "utopia-php/messaging": "0.12.*", "utopia-php/migration": "0.4.*", "utopia-php/orchestration": "0.9.*", - "utopia-php/platform": "0.7.*", + "utopia-php/platform": "0.5.*", "utopia-php/pools": "0.5.*", "utopia-php/preloader": "0.2.*", "utopia-php/queue": "0.7.*", diff --git a/composer.lock b/composer.lock index b9c45b149f..c86e058a8c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e002600539435ca8eaaace6e73b4004d", + "content-hash": "9ba1190efa21ae307532896397b3228d", "packages": [ { "name": "adhocore/jwt", @@ -2327,16 +2327,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.0", + "version": "0.5.2", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" + "reference": "b9feabc79b92dc2b05683a986ad43bce5c1583e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/b9feabc79b92dc2b05683a986ad43bce5c1583e3", + "reference": "b9feabc79b92dc2b05683a986ad43bce5c1583e3", "shasum": "" }, "require": { @@ -2344,8 +2344,7 @@ "ext-redis": "*", "php": ">=8.0", "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.7.*" + "utopia-php/framework": "0.33.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2371,9 +2370,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.0" + "source": "https://github.com/utopia-php/platform/tree/0.5.2" }, - "time": "2024-05-08T17:00:55+00:00" + "time": "2024-05-22T12:50:35+00:00" }, { "name": "utopia-php/pools", From 610edc95848da1fb0084e77b12e3a63cd0a8a8ed Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 16 Jun 2024 09:52:53 +0300 Subject: [PATCH 038/241] composer.lock --- composer.json | 2 +- composer.lock | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 9ba8db0e09..192b311822 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "utopia-php/messaging": "0.12.*", "utopia-php/migration": "0.4.*", "utopia-php/orchestration": "0.9.*", - "utopia-php/platform": "0.5.*", + "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", "utopia-php/preloader": "0.2.*", "utopia-php/queue": "0.7.*", diff --git a/composer.lock b/composer.lock index c86e058a8c..b9c45b149f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9ba1190efa21ae307532896397b3228d", + "content-hash": "e002600539435ca8eaaace6e73b4004d", "packages": [ { "name": "adhocore/jwt", @@ -2327,16 +2327,16 @@ }, { "name": "utopia-php/platform", - "version": "0.5.2", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "b9feabc79b92dc2b05683a986ad43bce5c1583e3" + "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/b9feabc79b92dc2b05683a986ad43bce5c1583e3", - "reference": "b9feabc79b92dc2b05683a986ad43bce5c1583e3", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", "shasum": "" }, "require": { @@ -2344,7 +2344,8 @@ "ext-redis": "*", "php": ">=8.0", "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.33.*" + "utopia-php/framework": "0.33.*", + "utopia-php/queue": "0.7.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2370,9 +2371,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.5.2" + "source": "https://github.com/utopia-php/platform/tree/0.7.0" }, - "time": "2024-05-22T12:50:35+00:00" + "time": "2024-05-08T17:00:55+00:00" }, { "name": "utopia-php/pools", From d39060e0a8740b9dc871cdddeb1ef10b9b4a9fd5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 16 Jun 2024 12:49:34 +0300 Subject: [PATCH 039/241] getDevice --- app/init.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/init.php b/app/init.php index c201c0b4a3..04836b0ea1 100644 --- a/app/init.php +++ b/app/init.php @@ -1451,9 +1451,9 @@ App::setResource('deviceForBuilds', function ($project) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); }, ['project']); -function getDevice(string $root, ?string $connection = null): Device +function getDevice(string $root, string $connection = ''): Device { - $connection = empty($connection) ? System::getEnv('_APP_CONNECTIONS_STORAGE', '') : System::getEnv($connection, ''); + $connection = !empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', ''); if (!empty($connection)) { $acl = 'private'; From f1bff1b62cc3bbce200e04349099be5cdbb4f1e6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 11:09:37 +0300 Subject: [PATCH 040/241] add migrations document --- src/Appwrite/Platform/Workers/Migrations.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 3d955cc757..cdaefe41fc 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -34,6 +34,7 @@ class Migrations extends Action protected Database $dbForProject; protected Database $dbForConsole; protected Document $project; + protected Document $migration; public static function getName(): string { @@ -81,6 +82,7 @@ class Migrations extends Action $this->dbForProject = $dbForProject; $this->dbForConsole = $dbForConsole; $this->project = $project; + $this->migration = $migration; /** * Handle Event execution. From ec71fb22de19a973eeb1e5b48d137e0abb913ed9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 11:45:07 +0300 Subject: [PATCH 041/241] Change processSource processDestination params --- src/Appwrite/Platform/Workers/Migrations.php | 35 ++++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index cdaefe41fc..19fd92e3fe 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -98,13 +98,15 @@ class Migrations extends Action } /** - * @param string $source - * @param array $credentials + * @param Document $migration * @return Source * @throws Exception */ - protected function processSource(string $source, array $credentials): Source + protected function processSource(Document $migration): Source { + $source = $migration->getAttribute('source'); + $credentials = $migration->getAttribute('credentials'); + return match ($source) { Firebase::getName() => new Firebase( json_decode($credentials['serviceAccount'], true), @@ -137,13 +139,15 @@ class Migrations extends Action } /** - * @param string $destination + * @param Document $migration * @param array $credentials * @return Destination * @throws Exception */ - protected function processDestination(string $destination, array $credentials): Destination + protected function processDestination(Document $migration, array $credentials): Destination { + $destination = $migration->getAttribute('destination'); + return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( $credentials['projectId'], @@ -284,19 +288,30 @@ class Migrations extends Action $log->addTag('type', $migrationDocument->getAttribute('source')); - $source = $this->processSource( - $migrationDocument->getAttribute('source'), - $migrationDocument->getAttribute('credentials') - ); + $source = $this->processSource($migrationDocument); $destination = $this->processDestination( - $migrationDocument->getAttribute('destination'), + $migrationDocument, [ 'projectId' => $projectDocument->getId(), 'endpoint' => 'http://appwrite/v1', 'apiKey' => $tempAPIKey['secret'] ] ); +// +// $source = $this->processSource( +// $migrationDocument->getAttribute('source'), +// $migrationDocument->getAttribute('credentials') +// ); +// +// $destination = $this->processDestination( +// $migrationDocument->getAttribute('destination'), +// [ +// 'projectId' => $projectDocument->getId(), +// 'endpoint' => 'http://appwrite/v1', +// 'apiKey' => $tempAPIKey['secret'] +// ] +// ); $source->report(); From 2d1299bb468de55ccca633e8d6a1603b83d8a39b Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 11:46:32 +0300 Subject: [PATCH 042/241] Remove migration --- src/Appwrite/Platform/Workers/Migrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 19fd92e3fe..458d19ca84 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -34,7 +34,7 @@ class Migrations extends Action protected Database $dbForProject; protected Database $dbForConsole; protected Document $project; - protected Document $migration; + //protected Document $migration; public static function getName(): string { @@ -82,7 +82,7 @@ class Migrations extends Action $this->dbForProject = $dbForProject; $this->dbForConsole = $dbForConsole; $this->project = $project; - $this->migration = $migration; + //$this->migration = $migration; /** * Handle Event execution. From 2ba6916e535c11665634d35c8de92dd815eb59f2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 12:03:32 +0300 Subject: [PATCH 043/241] Undefineds --- src/Appwrite/Platform/Workers/Migrations.php | 74 ++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 458d19ca84..9f6ba39df8 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -272,26 +272,28 @@ class Migrations extends Action * @throws Restricted * @throws Structure * @throws \Utopia\Database\Exception + * @throws Exception */ protected function processMigration(Document $migration, Log $log): void { $project = $this->project; $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); $tempAPIKey = $this->generateAPIKey($projectDocument); + $transfer = $source = $destination = null; try { - $migrationDocument = $this->dbForProject->getDocument('migrations', $migration->getId()); - $migrationDocument->setAttribute('stage', 'processing'); - $migrationDocument->setAttribute('status', 'processing'); + $migration = $this->dbForProject->getDocument('migrations', $migration->getId()); + $migration->setAttribute('stage', 'processing'); + $migration->setAttribute('status', 'processing'); $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'processing'", \microtime(true))); - $this->updateMigrationDocument($migrationDocument, $projectDocument); + $this->updateMigrationDocument($migration, $projectDocument); - $log->addTag('type', $migrationDocument->getAttribute('source')); + $log->addTag('type', $migration->getAttribute('source')); - $source = $this->processSource($migrationDocument); + $source = $this->processSource($migration); $destination = $this->processDestination( - $migrationDocument, + $migration, [ 'projectId' => $projectDocument->getId(), 'endpoint' => 'http://appwrite/v1', @@ -300,12 +302,12 @@ class Migrations extends Action ); // // $source = $this->processSource( -// $migrationDocument->getAttribute('source'), -// $migrationDocument->getAttribute('credentials') +// $migration->getAttribute('source'), +// $migration->getAttribute('credentials') // ); // // $destination = $this->processDestination( -// $migrationDocument->getAttribute('destination'), +// $migration->getAttribute('destination'), // [ // 'projectId' => $projectDocument->getId(), // 'endpoint' => 'http://appwrite/v1', @@ -321,17 +323,17 @@ class Migrations extends Action ); /** Start Transfer */ - $migrationDocument->setAttribute('stage', 'migrating'); + $migration->setAttribute('stage', 'migrating'); $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); - $this->updateMigrationDocument($migrationDocument, $projectDocument); + $this->updateMigrationDocument($migration, $projectDocument); $destination->init(); - $transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) { - $migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache())); - $migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); + $transfer->run($migration->getAttribute('resources'), function () use ($migration, $transfer, $projectDocument) { + $migration->setAttribute('resourceData', json_encode($transfer->getCache())); + $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - $this->updateMigrationDocument($migrationDocument, $projectDocument); + $this->updateMigrationDocument($migration, $projectDocument); }); $destination->shutDown(); @@ -340,8 +342,8 @@ class Migrations extends Action $destinationErrors = $destination->getErrors(); if (!empty($sourceErrors) || !empty($destinationErrors)) { - $migrationDocument->setAttribute('status', 'failed'); - $migrationDocument->setAttribute('stage', 'finished'); + $migration->setAttribute('status', 'failed'); + $migration->setAttribute('stage', 'finished'); $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and failed", \microtime(true))); $errorMessages = []; @@ -354,25 +356,26 @@ class Migrations extends Action $errorMessages[] = "Error occurred while pushing '{$error->getResourceType()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } - $migrationDocument->setAttribute('errors', $errorMessages); + $migration->setAttribute('errors', $errorMessages); $log->addExtra('migrationErrors', json_encode($errorMessages)); - $this->updateMigrationDocument($migrationDocument, $projectDocument); + $this->updateMigrationDocument($migration, $projectDocument); return; } - $migrationDocument->setAttribute('status', 'completed'); - $migrationDocument->setAttribute('stage', 'finished'); + $migration->setAttribute('status', 'completed'); + $migration->setAttribute('stage', 'finished'); $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and succeeded", \microtime(true))); } catch (\Throwable $th) { - Console::error($th->getMessage()); - if ($migrationDocument) { - Console::error($th->getMessage()); - Console::error($th->getTraceAsString()); - $migrationDocument->setAttribute('status', 'failed'); - $migrationDocument->setAttribute('stage', 'finished'); - $migrationDocument->setAttribute('errors', [$th->getMessage()]); + Console::error($th->getMessage()); + Console::error($th->getMessage()); + Console::error($th->getTraceAsString()); + + if (!$migration->isEmpty()) { + $migration->setAttribute('status', 'failed'); + $migration->setAttribute('stage', 'finished'); + $migration->setAttribute('errors', [$th->getMessage()]); return; } @@ -391,19 +394,18 @@ class Migrations extends Action $errorMessages[] = "Error occurred while pushing '{$error->getResourceType()}:{$error->getResourceId()}' to destination with message '{$error->getMessage()}'"; } - $migrationDocument->setAttribute('errors', $errorMessages); + $migration->setAttribute('errors', $errorMessages); $log->addTag('migrationErrors', json_encode($errorMessages)); } } finally { - if ($tempAPIKey) { + if (!$tempAPIKey->isEmpty()) { $this->removeAPIKey($tempAPIKey); } - if ($migrationDocument) { - $this->updateMigrationDocument($migrationDocument, $projectDocument); - if ($migrationDocument->getAttribute('status', '') == 'failed') { - throw new Exception("Migration failed"); - } + $this->updateMigrationDocument($migration, $projectDocument); + + if ($migration->getAttribute('status', '') == 'failed') { + throw new Exception("Migration failed"); } } } From ef6bc042fdba042ff01ddf07feeac52319b1a00d Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:11:21 +0300 Subject: [PATCH 044/241] Migrations 0.4.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 192b311822..7de6b86d67 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.4.*", + "utopia-php/migration": "dev-backups as 0.4.4", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From e457ed194f2b76b91d303660edacf3c1e3864b16 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:13:56 +0300 Subject: [PATCH 045/241] Minimum stable --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7de6b86d67..db9d5ca953 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-backups as 0.4.4", + "utopia-php/migration": "dev-backups as 1.4.4", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From 33a3674989164b1b264ab51346caed0616bba490 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:14:57 +0300 Subject: [PATCH 046/241] 0.4.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db9d5ca953..7de6b86d67 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-backups as 1.4.4", + "utopia-php/migration": "dev-backups as 0.4.4", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From b31a1e25847f195ad607036e3353ba56d95ecb3e Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:17:14 +0300 Subject: [PATCH 047/241] Remove comments --- src/Appwrite/Platform/Workers/Migrations.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 9f6ba39df8..dff23e7802 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -300,20 +300,6 @@ class Migrations extends Action 'apiKey' => $tempAPIKey['secret'] ] ); -// -// $source = $this->processSource( -// $migration->getAttribute('source'), -// $migration->getAttribute('credentials') -// ); -// -// $destination = $this->processDestination( -// $migration->getAttribute('destination'), -// [ -// 'projectId' => $projectDocument->getId(), -// 'endpoint' => 'http://appwrite/v1', -// 'apiKey' => $tempAPIKey['secret'] -// ] -// ); $source->report(); From 4c4b95a39f384877e700d8c4212be451a8923d96 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:19:44 +0300 Subject: [PATCH 048/241] composer.lock --- composer.lock | 87 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/composer.lock b/composer.lock index b9c45b149f..e8e687c455 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e002600539435ca8eaaace6e73b4004d", + "content-hash": "17035c3cd6a671688ab9b18a6c69e0a5", "packages": [ { "name": "adhocore/jwt", @@ -1569,16 +1569,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.0", + "version": "0.10.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "313bcdfbb166f75c2c205a59d1467cead63a9626" + "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/313bcdfbb166f75c2c205a59d1467cead63a9626", - "reference": "313bcdfbb166f75c2c205a59d1467cead63a9626", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/87ee4fc91e50d4ddfef650aa999ea12be3a99583", + "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583", "shasum": "" }, "require": { @@ -1613,9 +1613,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.0" + "source": "https://github.com/utopia-php/cache/tree/0.10.1" }, - "time": "2024-06-05T16:40:43+00:00" + "time": "2024-06-18T13:20:25+00:00" }, { "name": "utopia-php/cli", @@ -1719,16 +1719,16 @@ }, { "name": "utopia-php/database", - "version": "0.49.12", + "version": "0.49.13", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "45def2f7c6bc5f631dbb67e5df0e8e7331af5f63" + "reference": "fff42e0bd1db5a03d8c5df4302d72443bde3b860" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/45def2f7c6bc5f631dbb67e5df0e8e7331af5f63", - "reference": "45def2f7c6bc5f631dbb67e5df0e8e7331af5f63", + "url": "https://api.github.com/repos/utopia-php/database/zipball/fff42e0bd1db5a03d8c5df4302d72443bde3b860", + "reference": "fff42e0bd1db5a03d8c5df4302d72443bde3b860", "shasum": "" }, "require": { @@ -1769,9 +1769,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.49.12" + "source": "https://github.com/utopia-php/database/tree/0.49.13" }, - "time": "2024-06-05T16:52:59+00:00" + "time": "2024-06-18T14:33:55+00:00" }, { "name": "utopia-php/domains", @@ -2170,26 +2170,34 @@ }, { "name": "utopia-php/migration", - "version": "0.4.4", + "version": "dev-backups", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33" + "reference": "ce55838583cc237aabd9bdfc898229f17cef130c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/a8a5d392bebf082faf289f4dfe09d9fd76844c33", - "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/ce55838583cc237aabd9bdfc898229f17cef130c", + "reference": "ce55838583cc237aabd9bdfc898229f17cef130c", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "10.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3", + "utopia-php/cli": "0.15.*", + "utopia-php/database": "0.49.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "0.33.*", + "utopia-php/storage": "0.18.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "vlucas/phpdotenv": "5.*" + "laravel/pint": "1.12.*", + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "10.5.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2211,9 +2219,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.4.4" + "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-05-17T05:25:31+00:00" + "time": "2024-06-17T08:26:44+00:00" }, { "name": "utopia-php/mongo", @@ -3156,16 +3164,16 @@ }, { "name": "laravel/pint", - "version": "v1.16.0", + "version": "v1.16.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98" + "reference": "9266a47f1b9231b83e0cfd849009547329d871b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", + "url": "https://api.github.com/repos/laravel/pint/zipball/9266a47f1b9231b83e0cfd849009547329d871b1", + "reference": "9266a47f1b9231b83e0cfd849009547329d871b1", "shasum": "" }, "require": { @@ -3176,13 +3184,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.57.1", - "illuminate/view": "^10.48.10", - "larastan/larastan": "^2.9.6", + "friendsofphp/php-cs-fixer": "^3.59.3", + "illuminate/view": "^10.48.12", + "larastan/larastan": "^2.9.7", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.7" + "pestphp/pest": "^2.34.8" }, "bin": [ "builds/pint" @@ -3218,7 +3226,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-05-21T18:08:25+00:00" + "time": "2024-06-18T16:50:05+00:00" }, { "name": "matthiasmullie/minify", @@ -5589,9 +5597,18 @@ "time": "2023-11-21T18:54:41+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/migration", + "version": "dev-backups", + "alias": "0.4.4", + "alias_normalized": "0.4.4.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From b84fe6891c5b2600beeef56a2bcde4d5196912f4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 13:26:52 +0300 Subject: [PATCH 049/241] lint --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 392d232f8b..3904f94fb9 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -25,7 +25,7 @@ abstract class ScheduleBase extends Action abstract public static function getName(): string; abstract public static function getSupportedResource(): string; abstract public static function getCollectionId(): string; - abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB):void; + abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void; public function __construct() { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index dff23e7802..b257cc9f2f 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -19,9 +19,9 @@ use Utopia\Logger\Log; use Utopia\Logger\Log\Breadcrumb; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; -use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Source; +use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Sources\Firebase; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; From 2412946939cbcfc0d9aa860b54b6f6fd1ec3d3fd Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 19 Jun 2024 17:26:48 +0300 Subject: [PATCH 050/241] Comment init $destination init --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index b257cc9f2f..a008a50510 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -313,7 +313,7 @@ class Migrations extends Action $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); - $destination->init(); + //$destination->init(); $transfer->run($migration->getAttribute('resources'), function () use ($migration, $transfer, $projectDocument) { $migration->setAttribute('resourceData', json_encode($transfer->getCache())); From 72c647fba837ed289b3c6479a9c294d236dd6fb7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 26 Jun 2024 10:54:57 +0300 Subject: [PATCH 051/241] Add source shutDown --- composer.lock | 97 ++++++++++---------- src/Appwrite/Platform/Workers/Migrations.php | 3 +- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/composer.lock b/composer.lock index e8e687c455..d38d0436af 100644 --- a/composer.lock +++ b/composer.lock @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -1128,16 +1129,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -1188,7 +1189,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -1204,20 +1205,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -1268,7 +1269,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -1284,7 +1285,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "thecodingmachine/safe", @@ -1569,16 +1570,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.1", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583" + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/87ee4fc91e50d4ddfef650aa999ea12be3a99583", - "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", "shasum": "" }, "require": { @@ -1613,9 +1614,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.1" + "source": "https://github.com/utopia-php/cache/tree/0.10.2" }, - "time": "2024-06-18T13:20:25+00:00" + "time": "2024-06-25T20:36:35+00:00" }, { "name": "utopia-php/cli", @@ -1719,16 +1720,16 @@ }, { "name": "utopia-php/database", - "version": "0.49.13", + "version": "0.49.14", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "fff42e0bd1db5a03d8c5df4302d72443bde3b860" + "reference": "415588c0b98edee9d72cdfe269ff79b14cd8f56d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/fff42e0bd1db5a03d8c5df4302d72443bde3b860", - "reference": "fff42e0bd1db5a03d8c5df4302d72443bde3b860", + "url": "https://api.github.com/repos/utopia-php/database/zipball/415588c0b98edee9d72cdfe269ff79b14cd8f56d", + "reference": "415588c0b98edee9d72cdfe269ff79b14cd8f56d", "shasum": "" }, "require": { @@ -1769,9 +1770,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.49.13" + "source": "https://github.com/utopia-php/database/tree/0.49.14" }, - "time": "2024-06-18T14:33:55+00:00" + "time": "2024-06-20T02:39:23+00:00" }, { "name": "utopia-php/domains", @@ -2174,16 +2175,16 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "ce55838583cc237aabd9bdfc898229f17cef130c" + "reference": "2b491d582f316ca7670c4c23f01259482c0ff2a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/ce55838583cc237aabd9bdfc898229f17cef130c", - "reference": "ce55838583cc237aabd9bdfc898229f17cef130c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/2b491d582f316ca7670c4c23f01259482c0ff2a3", + "reference": "2b491d582f316ca7670c4c23f01259482c0ff2a3", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.*", + "appwrite/appwrite": "11.1.*", "ext-curl": "*", "ext-openssl": "*", "php": "8.3", @@ -2221,7 +2222,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-06-17T08:26:44+00:00" + "time": "2024-06-26T07:48:24+00:00" }, { "name": "utopia-php/mongo", @@ -2996,16 +2997,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.38.7", + "version": "0.38.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a" + "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", - "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", + "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", "shasum": "" }, "require": { @@ -3041,9 +3042,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.38.7" + "source": "https://github.com/appwrite/sdk-generator/tree/0.38.8" }, - "time": "2024-06-10T00:23:02+00:00" + "time": "2024-06-17T00:42:27+00:00" }, { "name": "doctrine/deprecations", @@ -5348,16 +5349,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -5407,7 +5408,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -5423,7 +5424,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "textalk/websocket", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index a008a50510..a64fdbdc56 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -313,8 +313,6 @@ class Migrations extends Action $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); - //$destination->init(); - $transfer->run($migration->getAttribute('resources'), function () use ($migration, $transfer, $projectDocument) { $migration->setAttribute('resourceData', json_encode($transfer->getCache())); $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); @@ -323,6 +321,7 @@ class Migrations extends Action }); $destination->shutDown(); + $source->shutDown(); $sourceErrors = $source->getErrors(); $destinationErrors = $destination->getErrors(); From ed1a5b600dfac78f4f2a54587211ca2dd0bde9a8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 10 Jul 2024 19:00:10 +1200 Subject: [PATCH 052/241] Fix oldAttribute key length --- app/controllers/api/databases.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index c8903538d0..446aecdda0 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2434,7 +2434,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') 'required' => true, 'array' => false, 'default' => null, - 'size' => 36 + 'size' => Database::LENGTH_KEY ]; $oldAttributes[] = [ From b4ab1c2c698662f59dfbc0bf69e58b5982f100ed Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jul 2024 16:34:05 +0300 Subject: [PATCH 053/241] Debug create document --- app/controllers/api/databases.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 446aecdda0..a0a31c1968 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2900,6 +2900,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $checkPermissions($collection, $document, Database::PERMISSION_CREATE); + var_dump($document); + try { $document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document); } catch (StructureException $exception) { From 865bfdbd34fc1e583f7f2b2569bf1d0954beb24a Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 11 Jul 2024 12:38:55 +0300 Subject: [PATCH 054/241] DuplicateException dbg --- app/controllers/api/databases.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index a0a31c1968..bff7d0a1a2 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2907,6 +2907,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); } catch (DuplicateException $exception) { + var_dump($exception); throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } From 475e2a64cea02f59c7585ee04a1f00c5d9084352 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 11 Jul 2024 13:36:28 +0300 Subject: [PATCH 055/241] DuplicateException dbg --- app/controllers/api/databases.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index bff7d0a1a2..2c37e096c2 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2907,7 +2907,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); } catch (DuplicateException $exception) { - var_dump($exception); + var_dump('DuplicateException found'); + var_dump($exception->getMessage()); + var_dump($exception->getFile()); + var_dump($exception->getLine()); + var_dump("-----------------"); throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } From 6a717f22728d7cda0fd82d0e0eba382026854bde Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 11 Jul 2024 18:47:03 +0300 Subject: [PATCH 056/241] Add resourceId to migration collections.php --- app/config/collections.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 73dbc9eaa5..4533ce3e01 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4073,6 +4073,17 @@ $projectCollections = array_merge([ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('resourceId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('statusCounters'), 'type' => Database::VAR_STRING, From d9da0dd671526723dc15bc4185c4d6d6ea799198 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Jul 2024 17:22:36 +0300 Subject: [PATCH 057/241] Add resourceId to migration collections.php --- composer.lock | 32 ++++++++++---------- src/Appwrite/Platform/Workers/Migrations.php | 15 +++++---- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index d38d0436af..b1c23caf84 100644 --- a/composer.lock +++ b/composer.lock @@ -2175,12 +2175,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "2b491d582f316ca7670c4c23f01259482c0ff2a3" + "reference": "a7dc26f65866d022451a17860d50e29123edd7e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/2b491d582f316ca7670c4c23f01259482c0ff2a3", - "reference": "2b491d582f316ca7670c4c23f01259482c0ff2a3", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/a7dc26f65866d022451a17860d50e29123edd7e4", + "reference": "a7dc26f65866d022451a17860d50e29123edd7e4", "shasum": "" }, "require": { @@ -2222,7 +2222,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-06-26T07:48:24+00:00" + "time": "2024-07-15T11:52:33+00:00" }, { "name": "utopia-php/mongo", @@ -3165,16 +3165,16 @@ }, { "name": "laravel/pint", - "version": "v1.16.1", + "version": "v1.16.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9266a47f1b9231b83e0cfd849009547329d871b1" + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9266a47f1b9231b83e0cfd849009547329d871b1", - "reference": "9266a47f1b9231b83e0cfd849009547329d871b1", + "url": "https://api.github.com/repos/laravel/pint/zipball/51f1ba679a6afe0315621ad143d788bd7ded0eca", + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca", "shasum": "" }, "require": { @@ -3227,7 +3227,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-06-18T16:50:05+00:00" + "time": "2024-07-09T15:58:08+00:00" }, { "name": "matthiasmullie/minify", @@ -3415,16 +3415,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { @@ -3435,7 +3435,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -3467,9 +3467,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "phar-io/manifest", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index a64fdbdc56..f833642055 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -313,12 +313,15 @@ class Migrations extends Action $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); - $transfer->run($migration->getAttribute('resources'), function () use ($migration, $transfer, $projectDocument) { - $migration->setAttribute('resourceData', json_encode($transfer->getCache())); - $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - - $this->updateMigrationDocument($migration, $projectDocument); - }); + $transfer->run( + $migration->getAttribute('resources'), + function () use ($migration, $transfer, $projectDocument) { + $migration->setAttribute('resourceData', json_encode($transfer->getCache())); + $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); + $this->updateMigrationDocument($migration, $projectDocument); + }, + $migration->getAttribute('resourceId', '') + ); $destination->shutDown(); $source->shutDown(); From 865d84f4e75b2a03156f61785dacfc534c0af0e9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Jul 2024 17:28:40 +0300 Subject: [PATCH 058/241] ResourceId default value --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index f833642055..309a562a8a 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -320,7 +320,7 @@ class Migrations extends Action $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); $this->updateMigrationDocument($migration, $projectDocument); }, - $migration->getAttribute('resourceId', '') + $migration->getAttribute('resourceId') ); $destination->shutDown(); From 8de8ae03f38bbaf6350dd1279224c2950d1bb00d Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 22 Jul 2024 15:38:58 +0300 Subject: [PATCH 059/241] Add header x-appwrite-preserve-dates --- app/controllers/api/databases.php | 14 ++--- composer.lock | 29 +++++----- .../e2e/Services/Databases/DatabasesBase.php | 56 ++++++++++++++++++- 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 2c37e096c2..bd8a1cc444 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2743,11 +2743,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('data', [], new JSON(), 'Document data as JSON object.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->inject('response') + ->inject('request') ->inject('dbForProject') ->inject('user') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode) { + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Request $request, Database $dbForProject, Document $user, Event $queueForEvents, string $mode) { + + if ($request->getHeader('x-appwrite-preserve-dates') === 'true') { + $dbForProject->setPreserveDates(true); + } $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2900,18 +2905,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $checkPermissions($collection, $document, Database::PERMISSION_CREATE); - var_dump($document); - try { $document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document); } catch (StructureException $exception) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); } catch (DuplicateException $exception) { - var_dump('DuplicateException found'); - var_dump($exception->getMessage()); - var_dump($exception->getFile()); - var_dump($exception->getLine()); - var_dump("-----------------"); throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } diff --git a/composer.lock b/composer.lock index b1c23caf84..da60b31309 100644 --- a/composer.lock +++ b/composer.lock @@ -356,16 +356,16 @@ }, { "name": "chillerlan/php-settings-container", - "version": "2.1.5", + "version": "2.1.6", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "f705310389264c3578fdd9ffb15aa2cd6d91772e" + "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/f705310389264c3578fdd9ffb15aa2cd6d91772e", - "reference": "f705310389264c3578fdd9ffb15aa2cd6d91772e", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/5553558bd381fce5108c6d0343c12e488cfec6bb", + "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb", "shasum": "" }, "require": { @@ -373,15 +373,16 @@ "php": "^7.4 || ^8.0" }, "require-dev": { - "phan/phan": "^5.4", - "phpcsstandards/php_codesniffer": "^3.8", - "phpmd/phpmd": "^2.13", - "phpunit/phpunit": "^9.6" + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.10" }, "type": "library", "autoload": { "psr-4": { - "chillerlan\\Settings\\": "src/" + "chillerlan\\Settings\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -418,7 +419,7 @@ "type": "ko_fi" } ], - "time": "2024-01-05T23:20:55+00:00" + "time": "2024-07-17T01:04:28+00:00" }, { "name": "dragonmantank/cron-expression", @@ -2175,12 +2176,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "a7dc26f65866d022451a17860d50e29123edd7e4" + "reference": "d63024ce593124a5ec47f9606386bc01ba27424c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/a7dc26f65866d022451a17860d50e29123edd7e4", - "reference": "a7dc26f65866d022451a17860d50e29123edd7e4", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/d63024ce593124a5ec47f9606386bc01ba27424c", + "reference": "d63024ce593124a5ec47f9606386bc01ba27424c", "shasum": "" }, "require": { @@ -2222,7 +2223,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-07-15T11:52:33+00:00" + "time": "2024-07-21T12:03:39+00:00" }, { "name": "utopia-php/mongo", diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 6f65552f1c..1b0bd45a8a 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -4754,6 +4754,8 @@ trait DatabasesBase $this->assertEquals($longtext['headers']['status-code'], 202); + $longtext = file_get_contents(__DIR__ . '/../../../resources/longtext.txt'); + for ($i = 0; $i < 1; $i++) { $this->client->call(Client::METHOD_POST, '/databases/' . $data['databaseId'] . '/collections/' . $data['$id'] . '/documents', array_merge([ 'content-type' => 'application/json', @@ -4761,7 +4763,7 @@ trait DatabasesBase ], $this->getHeaders()), [ 'documentId' => ID::unique(), 'data' => [ - 'longtext' => file_get_contents(__DIR__ . '/../../../resources/longtext.txt'), + 'longtext' => $longtext . $longtext, ], 'permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), @@ -4783,4 +4785,56 @@ trait DatabasesBase $this->assertEquals(408, $response['headers']['status-code']); } + + /** + * @depends testCreateDatabase + */ + public function testPreserveDates(array $data): void + { + $databaseId = $data['databaseId']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'collectionId' => 'preserve_dates', + 'name' => 'Preserve Dates', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-preserve-dates' => 'true' + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + '$createdAt' => '2000-01-01 01:01:01', + '$updatedAt' => '2020-01-01 01:01:01', + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collection['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('$id', [$response['body']['$id']])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('2000-01-01T01:01:01.000+00:00', $response['body']['documents'][0]['$createdAt']); + $this->assertEquals('2020-01-01T01:01:01.000+00:00', $response['body']['documents'][0]['$updatedAt']); + } } From 6fb318c7882c9f32725c7418b2fe33d776c212fb Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 24 Jul 2024 12:04:11 +0300 Subject: [PATCH 060/241] policies scope --- app/config/scopes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/scopes.php b/app/config/scopes.php index 7243e35f96..7162dccc4f 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -130,10 +130,10 @@ return [ // List of publicly visible scopes 'assistant.read' => [ 'description' => 'Access to read the Assistant service', ], - 'backups.write' => [ + 'policies.write' => [ 'description' => 'Access to create, update, and delete your backups', ], - 'backups.read' => [ + 'policies.read' => [ 'description' => 'Access to read the backups service', ], ]; From b217221cefa0653e508594a907a43892d7d34906 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 25 Jul 2024 11:53:35 +0300 Subject: [PATCH 061/241] Database 0.50.* --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7de6b86d67..8190297215 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.49.*", + "utopia-php/database": "0.50.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From a0c57d20d8aab6ace2e7ff9aada2ed0a165ba39f Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 25 Jul 2024 11:58:15 +0300 Subject: [PATCH 062/241] Abuse Audit upgrade --- composer.json | 4 +-- composer.lock | 73 +++++++++++++++++++++++++++------------------------ 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/composer.json b/composer.json index 8190297215..d2d47c766b 100644 --- a/composer.json +++ b/composer.json @@ -44,9 +44,9 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.37.*", + "utopia-php/abuse": "0.39.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.39.*", + "utopia-php/audit": "0.40.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", diff --git a/composer.lock b/composer.lock index da60b31309..600ee19ad8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "17035c3cd6a671688ab9b18a6c69e0a5", + "content-hash": "a6904abee5dfb9496340caaa2c9f1f69", "packages": [ { "name": "adhocore/jwt", @@ -1429,26 +1429,28 @@ }, { "name": "utopia-php/abuse", - "version": "0.37.1", + "version": "0.39.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "4dfcff4754c7804d1a70039792c0f2d59a5cc981" + "reference": "55e184db4d89d93e9b557f29f95f3dea661907b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/4dfcff4754c7804d1a70039792c0f2d59a5cc981", - "reference": "4dfcff4754c7804d1a70039792c0f2d59a5cc981", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/55e184db4d89d93e9b557f29f95f3dea661907b0", + "reference": "55e184db4d89d93e9b557f29f95f3dea661907b0", "shasum": "" }, "require": { "ext-curl": "*", "ext-pdo": "*", + "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.49.*" + "utopia-php/database": "0.50.*" }, "require-dev": { "laravel/pint": "1.5.*", + "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^9.4" }, @@ -1472,9 +1474,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.37.1" + "source": "https://github.com/utopia-php/abuse/tree/0.39.0" }, - "time": "2024-06-05T18:03:59+00:00" + "time": "2024-07-07T20:51:30+00:00" }, { "name": "utopia-php/analytics", @@ -1524,21 +1526,21 @@ }, { "name": "utopia-php/audit", - "version": "0.39.1", + "version": "0.40.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "7ea91e0ceea7b94293612fea94022b73315677c2" + "reference": "735ae211ce5fee5b52b736731571b4030b1d7cdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/7ea91e0ceea7b94293612fea94022b73315677c2", - "reference": "7ea91e0ceea7b94293612fea94022b73315677c2", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/735ae211ce5fee5b52b736731571b4030b1d7cdc", + "reference": "735ae211ce5fee5b52b736731571b4030b1d7cdc", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.49.*" + "utopia-php/database": "0.50.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1565,9 +1567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.39.1" + "source": "https://github.com/utopia-php/audit/tree/0.40.0" }, - "time": "2024-06-05T19:28:22+00:00" + "time": "2024-06-24T00:52:17+00:00" }, { "name": "utopia-php/cache", @@ -1721,16 +1723,16 @@ }, { "name": "utopia-php/database", - "version": "0.49.14", + "version": "0.50.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "415588c0b98edee9d72cdfe269ff79b14cd8f56d" + "reference": "ce3eaccb2f3bbd34b2b97419836fec633b26b8f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/415588c0b98edee9d72cdfe269ff79b14cd8f56d", - "reference": "415588c0b98edee9d72cdfe269ff79b14cd8f56d", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ce3eaccb2f3bbd34b2b97419836fec633b26b8f7", + "reference": "ce3eaccb2f3bbd34b2b97419836fec633b26b8f7", "shasum": "" }, "require": { @@ -1771,9 +1773,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.49.14" + "source": "https://github.com/utopia-php/database/tree/0.50.0" }, - "time": "2024-06-20T02:39:23+00:00" + "time": "2024-06-21T03:21:42+00:00" }, { "name": "utopia-php/domains", @@ -2176,12 +2178,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "d63024ce593124a5ec47f9606386bc01ba27424c" + "reference": "3668d07abf45d1356f9dacc43a19283c055f0c57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/d63024ce593124a5ec47f9606386bc01ba27424c", - "reference": "d63024ce593124a5ec47f9606386bc01ba27424c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/3668d07abf45d1356f9dacc43a19283c055f0c57", + "reference": "3668d07abf45d1356f9dacc43a19283c055f0c57", "shasum": "" }, "require": { @@ -2189,16 +2191,17 @@ "ext-curl": "*", "ext-openssl": "*", "php": "8.3", - "utopia-php/cli": "0.15.*", - "utopia-php/database": "0.49.*", + "utopia-php/database": "0.50.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" }, "require-dev": { - "laravel/pint": "1.12.*", - "phpstan/phpstan": "^1.11", - "phpunit/phpunit": "10.5.*", + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.16.*", "vlucas/phpdotenv": "5.6.*" }, "type": "library", @@ -2223,7 +2226,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-07-21T12:03:39+00:00" + "time": "2024-07-25T02:50:15+00:00" }, { "name": "utopia-php/mongo", @@ -3166,16 +3169,16 @@ }, { "name": "laravel/pint", - "version": "v1.16.2", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca" + "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/51f1ba679a6afe0315621ad143d788bd7ded0eca", - "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca", + "url": "https://api.github.com/repos/laravel/pint/zipball/4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", + "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", "shasum": "" }, "require": { @@ -3228,7 +3231,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-07-09T15:58:08+00:00" + "time": "2024-07-23T16:40:20+00:00" }, { "name": "matthiasmullie/minify", From d67e9eec3424ecc5bdf21f58ddf78cc4275d2eaa Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 25 Jul 2024 12:08:30 +0300 Subject: [PATCH 063/241] composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d2d47c766b..b40b32f979 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-backups as 0.4.4", + "utopia-php/migration": "dev-backups as 0.4.999", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From 203021ecb76495fb54e00b6b2411d2fd039bdf2b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 25 Jul 2024 12:38:54 +0300 Subject: [PATCH 064/241] "utopia-php/abuse": "0.38.*", --- composer.json | 2 +- composer.lock | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index b40b32f979..4e04807374 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.39.*", + "utopia-php/abuse": "0.38.*", "utopia-php/analytics": "0.10.*", "utopia-php/audit": "0.40.*", "utopia-php/cache": "0.10.*", diff --git a/composer.lock b/composer.lock index 600ee19ad8..bbdf6a6e21 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6904abee5dfb9496340caaa2c9f1f69", + "content-hash": "afb5cff358a14fb833338c25d6337864", "packages": [ { "name": "adhocore/jwt", @@ -1429,28 +1429,26 @@ }, { "name": "utopia-php/abuse", - "version": "0.39.0", + "version": "0.38.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "55e184db4d89d93e9b557f29f95f3dea661907b0" + "reference": "b7be9086c9d9b4561d810cbd42fdda798742f56c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/55e184db4d89d93e9b557f29f95f3dea661907b0", - "reference": "55e184db4d89d93e9b557f29f95f3dea661907b0", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/b7be9086c9d9b4561d810cbd42fdda798742f56c", + "reference": "b7be9086c9d9b4561d810cbd42fdda798742f56c", "shasum": "" }, "require": { "ext-curl": "*", "ext-pdo": "*", - "ext-redis": "*", "php": ">=8.0", "utopia-php/database": "0.50.*" }, "require-dev": { "laravel/pint": "1.5.*", - "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^9.4" }, @@ -1474,9 +1472,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.39.0" + "source": "https://github.com/utopia-php/abuse/tree/0.38.0" }, - "time": "2024-07-07T20:51:30+00:00" + "time": "2024-06-24T00:52:02+00:00" }, { "name": "utopia-php/analytics", @@ -5606,8 +5604,8 @@ { "package": "utopia-php/migration", "version": "dev-backups", - "alias": "0.4.4", - "alias_normalized": "0.4.4.0" + "alias": "0.4.999", + "alias_normalized": "0.4.999.0" } ], "minimum-stability": "stable", From 1ff717eceff42b4d49c833bdaa109de1d725e847 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 25 Jul 2024 14:34:19 +0300 Subject: [PATCH 065/241] Change to getResourceName --- src/Appwrite/Platform/Workers/Migrations.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 309a562a8a..ba256b0905 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -337,11 +337,11 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { /** @var MigrationException $error */ - $errorMessages[] = "Error occurred while fetching '{$error->getResourceType()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; + $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { /** @var MigrationException $error */ - $errorMessages[] = "Error occurred while pushing '{$error->getResourceType()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; + $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } $migration->setAttribute('errors', $errorMessages); @@ -375,11 +375,11 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { /** @var MigrationException $error */ - $errorMessages[] = "Error occurred while fetching '{$error->getResourceType()}:{$error->getResourceId()}' from source with message '{$error->getMessage()}'"; + $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { /** @var MigrationException $error */ - $errorMessages[] = "Error occurred while pushing '{$error->getResourceType()}:{$error->getResourceId()}' to destination with message '{$error->getMessage()}'"; + $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message '{$error->getMessage()}'"; } $migration->setAttribute('errors', $errorMessages); From 965cf9aac68fa92ca7848d97247da48e8e076851 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 09:27:18 +0300 Subject: [PATCH 066/241] processDestination params --- src/Appwrite/Platform/Workers/Migrations.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index ba256b0905..980fb850fc 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -8,6 +8,7 @@ use Appwrite\Permission; use Appwrite\Role; use Exception; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; @@ -152,7 +153,9 @@ class Migrations extends Action DestinationAppwrite::getName() => new DestinationAppwrite( $credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], - $credentials['apiKey'] + $credentials['apiKey'], + $this->dbForProject, + Config::getParam('collections', [])['databases']['collections'] ), default => throw new \Exception('Invalid destination type'), }; From a98ff7118813dc6bd5dcf285091cb937b66db070 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 09:29:02 +0300 Subject: [PATCH 067/241] processDestination constructor --- src/Appwrite/Platform/Workers/Migrations.php | 45 +++++++------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 980fb850fc..27e62d0364 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -33,7 +33,9 @@ use Utopia\Queue\Message; class Migrations extends Action { protected Database $dbForProject; + protected Database $dbForConsole; + protected Document $project; //protected Document $migration; @@ -57,11 +59,6 @@ class Migrations extends Action } /** - * @param Message $message - * @param Database $dbForProject - * @param Database $dbForConsole - * @param Log $log - * @return void * @throws Exception */ public function action(Message $message, Database $dbForProject, Database $dbForConsole, Log $log): void @@ -72,8 +69,8 @@ class Migrations extends Action throw new Exception('Missing payload'); } - $events = $payload['events'] ?? []; - $project = new Document($payload['project'] ?? []); + $events = $payload['events'] ?? []; + $project = new Document($payload['project'] ?? []); $migration = new Document($payload['migration'] ?? []); if ($project->getId() === 'console') { @@ -99,8 +96,6 @@ class Migrations extends Action } /** - * @param Document $migration - * @return Source * @throws Exception */ protected function processSource(Document $migration): Source @@ -140,9 +135,6 @@ class Migrations extends Action } /** - * @param Document $migration - * @param array $credentials - * @return Destination * @throws Exception */ protected function processDestination(Document $migration, array $credentials): Destination @@ -155,7 +147,7 @@ class Migrations extends Action str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey'], $this->dbForProject, - Config::getParam('collections', [])['databases']['collections'] + Config::getParam('collections', [])['databases']['collections'], ), default => throw new \Exception('Invalid destination type'), }; @@ -201,8 +193,6 @@ class Migrations extends Action } /** - * @param Document $apiKey - * @return void * @throws \Utopia\Database\Exception * @throws Authorization * @throws Conflict @@ -215,8 +205,6 @@ class Migrations extends Action } /** - * @param Document $project - * @return Document * @throws Authorization * @throws Structure * @throws \Utopia\Database\Exception @@ -267,9 +255,6 @@ class Migrations extends Action } /** - * @param Document $migration - * @param Log $log - * @return void * @throws Authorization * @throws Conflict * @throws Restricted @@ -288,7 +273,7 @@ class Migrations extends Action $migration = $this->dbForProject->getDocument('migrations', $migration->getId()); $migration->setAttribute('stage', 'processing'); $migration->setAttribute('status', 'processing'); - $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'processing'", \microtime(true))); + $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'processing'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); $log->addTag('type', $migration->getAttribute('source')); @@ -299,8 +284,8 @@ class Migrations extends Action $migration, [ 'projectId' => $projectDocument->getId(), - 'endpoint' => 'http://appwrite/v1', - 'apiKey' => $tempAPIKey['secret'] + 'endpoint' => 'http://appwrite/v1', + 'apiKey' => $tempAPIKey['secret'], ] ); @@ -313,7 +298,7 @@ class Migrations extends Action /** Start Transfer */ $migration->setAttribute('stage', 'migrating'); - $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true))); + $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); $transfer->run( @@ -332,10 +317,10 @@ class Migrations extends Action $sourceErrors = $source->getErrors(); $destinationErrors = $destination->getErrors(); - if (!empty($sourceErrors) || !empty($destinationErrors)) { + if (! empty($sourceErrors) || ! empty($destinationErrors)) { $migration->setAttribute('status', 'failed'); $migration->setAttribute('stage', 'finished'); - $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and failed", \microtime(true))); + $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'finished' and failed", \microtime(true))); $errorMessages = []; foreach ($sourceErrors as $error) { @@ -356,14 +341,14 @@ class Migrations extends Action $migration->setAttribute('status', 'completed'); $migration->setAttribute('stage', 'finished'); - $log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and succeeded", \microtime(true))); + $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'finished' and succeeded", \microtime(true))); } catch (\Throwable $th) { Console::error($th->getMessage()); Console::error($th->getMessage()); Console::error($th->getTraceAsString()); - if (!$migration->isEmpty()) { + if (! $migration->isEmpty()) { $migration->setAttribute('status', 'failed'); $migration->setAttribute('stage', 'finished'); $migration->setAttribute('errors', [$th->getMessage()]); @@ -389,14 +374,14 @@ class Migrations extends Action $log->addTag('migrationErrors', json_encode($errorMessages)); } } finally { - if (!$tempAPIKey->isEmpty()) { + if (! $tempAPIKey->isEmpty()) { $this->removeAPIKey($tempAPIKey); } $this->updateMigrationDocument($migration, $projectDocument); if ($migration->getAttribute('status', '') == 'failed') { - throw new Exception("Migration failed"); + throw new Exception('Migration failed'); } } } From 854f243fb1e9704c5de92620829ce2f3cce6b8cf Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 09:51:22 +0300 Subject: [PATCH 068/241] dbg error --- src/Appwrite/Platform/Workers/Migrations.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 27e62d0364..9fe1d1fd86 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -329,6 +329,13 @@ class Migrations extends Action } foreach ($destinationErrors as $error) { /** @var MigrationException $error */ + var_dump('$destinationErrors ====== '); + var_dump($error->getMessage()); + var_dump($error->getLine()); + var_dump($error->getFile()); + var_dump($error->getPrevious()->getMessage()); + var_dump($error->getPrevious()->getLine()); + var_dump($error->getPrevious()->getFile()); $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } From 5620f360c8a023e03738b8d6944f2693211701fa Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 09:52:47 +0300 Subject: [PATCH 069/241] dbg error --- src/Appwrite/Platform/Workers/Migrations.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 9fe1d1fd86..00037ec258 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -324,7 +324,14 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { - /** @var MigrationException $error */ + /** @var $sourceErrors $error */ + var_dump('$sourceErrors ====== '); + var_dump($error->getMessage()); + var_dump($error->getLine()); + var_dump($error->getFile()); + var_dump($error->getPrevious()->getMessage()); + var_dump($error->getPrevious()->getLine()); + var_dump($error->getPrevious()->getFile()); $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { From 1a97848d9cd835ec6d8da41f2781833c153169f8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 11:58:04 +0300 Subject: [PATCH 070/241] getTraceAsString --- src/Appwrite/Platform/Workers/Migrations.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 00037ec258..442f639fae 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -343,6 +343,7 @@ class Migrations extends Action var_dump($error->getPrevious()->getMessage()); var_dump($error->getPrevious()->getLine()); var_dump($error->getPrevious()->getFile()); + var_dump($error->getPrevious()->getTraceAsString()); $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } From ebb8d4a23b7ed35596db501b71999ceef5c46c2a Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 11:58:36 +0300 Subject: [PATCH 071/241] getTraceAsString --- src/Appwrite/Platform/Workers/Migrations.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 442f639fae..5d6c62a2c8 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -340,6 +340,7 @@ class Migrations extends Action var_dump($error->getMessage()); var_dump($error->getLine()); var_dump($error->getFile()); + var_dump($error->getTraceAsString()); var_dump($error->getPrevious()->getMessage()); var_dump($error->getPrevious()->getLine()); var_dump($error->getPrevious()->getFile()); From 4f87d72bcd80bab98ac89e47b406cb51d3773022 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 28 Jul 2024 14:44:37 +0300 Subject: [PATCH 072/241] Migrations errors --- src/Appwrite/Platform/Workers/Migrations.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 5d6c62a2c8..4474f3dda9 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -325,26 +325,10 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { /** @var $sourceErrors $error */ - var_dump('$sourceErrors ====== '); - var_dump($error->getMessage()); - var_dump($error->getLine()); - var_dump($error->getFile()); - var_dump($error->getPrevious()->getMessage()); - var_dump($error->getPrevious()->getLine()); - var_dump($error->getPrevious()->getFile()); $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { /** @var MigrationException $error */ - var_dump('$destinationErrors ====== '); - var_dump($error->getMessage()); - var_dump($error->getLine()); - var_dump($error->getFile()); - var_dump($error->getTraceAsString()); - var_dump($error->getPrevious()->getMessage()); - var_dump($error->getPrevious()->getLine()); - var_dump($error->getPrevious()->getFile()); - var_dump($error->getPrevious()->getTraceAsString()); $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } From 0ed7827fe49b78bdcbe2ce4189925174b723de40 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 30 Jul 2024 10:26:13 +0300 Subject: [PATCH 073/241] Add backups scopes --- app/config/scopes.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/config/scopes.php b/app/config/scopes.php index 7162dccc4f..181562f1eb 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -136,4 +136,13 @@ return [ // List of publicly visible scopes 'policies.read' => [ 'description' => 'Access to read the backups service', ], + 'archives.read' => [ + 'description' => 'Access to read the backups service', + ], + 'archives.write' => [ + 'description' => 'Access to create, update, and delete your archive', + ], + 'restorations.write' => [ + 'description' => 'Access to create, update, and delete your restoration', + ], ]; From 85b43d1f5c29c5670b286081ddb985e1c18e4574 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 5 Aug 2024 14:48:47 +0300 Subject: [PATCH 074/241] restorations.read --- app/config/scopes.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/config/scopes.php b/app/config/scopes.php index 181562f1eb..99faa59e54 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -142,6 +142,9 @@ return [ // List of publicly visible scopes 'archives.write' => [ 'description' => 'Access to create, update, and delete your archive', ], + 'restorations.read' => [ + 'description' => 'Access to read your restorations', + ], 'restorations.write' => [ 'description' => 'Access to create, update, and delete your restoration', ], From 0bb34e0d01a7caf24607ab144006d3b284d0c7a0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 Aug 2024 12:46:13 +0300 Subject: [PATCH 075/241] Add resourceType --- composer.lock | 42 ++++++++++---------- src/Appwrite/Platform/Workers/Migrations.php | 3 +- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index bbdf6a6e21..6949a53726 100644 --- a/composer.lock +++ b/composer.lock @@ -1721,16 +1721,16 @@ }, { "name": "utopia-php/database", - "version": "0.50.0", + "version": "0.50.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ce3eaccb2f3bbd34b2b97419836fec633b26b8f7" + "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ce3eaccb2f3bbd34b2b97419836fec633b26b8f7", - "reference": "ce3eaccb2f3bbd34b2b97419836fec633b26b8f7", + "url": "https://api.github.com/repos/utopia-php/database/zipball/c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", + "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", "shasum": "" }, "require": { @@ -1771,9 +1771,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.50.0" + "source": "https://github.com/utopia-php/database/tree/0.50.2" }, - "time": "2024-06-21T03:21:42+00:00" + "time": "2024-07-31T10:12:19+00:00" }, { "name": "utopia-php/domains", @@ -1923,16 +1923,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.6", + "version": "0.33.7", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6" + "reference": "78d293d99a262bd63ece750bbf989c7e0643b825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", + "url": "https://api.github.com/repos/utopia-php/http/zipball/78d293d99a262bd63ece750bbf989c7e0643b825", + "reference": "78d293d99a262bd63ece750bbf989c7e0643b825", "shasum": "" }, "require": { @@ -1962,9 +1962,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.6" + "source": "https://github.com/utopia-php/http/tree/0.33.7" }, - "time": "2024-03-21T18:10:57+00:00" + "time": "2024-08-01T14:01:04+00:00" }, { "name": "utopia-php/image", @@ -2176,12 +2176,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "3668d07abf45d1356f9dacc43a19283c055f0c57" + "reference": "ae6151e7a65fc5bf253461cd8db909001d7549e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/3668d07abf45d1356f9dacc43a19283c055f0c57", - "reference": "3668d07abf45d1356f9dacc43a19283c055f0c57", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/ae6151e7a65fc5bf253461cd8db909001d7549e9", + "reference": "ae6151e7a65fc5bf253461cd8db909001d7549e9", "shasum": "" }, "require": { @@ -2224,7 +2224,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-07-25T02:50:15+00:00" + "time": "2024-08-06T09:32:45+00:00" }, { "name": "utopia-php/mongo", @@ -3167,16 +3167,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5" + "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", + "url": "https://api.github.com/repos/laravel/pint/zipball/b5b6f716db298671c1dfea5b1082ec2c0ae7064f", + "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f", "shasum": "" }, "require": { @@ -3229,7 +3229,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-07-23T16:40:20+00:00" + "time": "2024-08-01T09:06:33+00:00" }, { "name": "matthiasmullie/minify", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 4474f3dda9..d80df6b1a0 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -308,7 +308,8 @@ class Migrations extends Action $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); $this->updateMigrationDocument($migration, $projectDocument); }, - $migration->getAttribute('resourceId') + $migration->getAttribute('resourceId'), + $migration->getAttribute('resourceType') ); $destination->shutDown(); From 96274e9c77cf6e15b67a616c70f6dfd34681af5f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 Aug 2024 13:55:59 +0300 Subject: [PATCH 076/241] Collections resourceType --- app/config/collections.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 4533ce3e01..29e19d4967 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4084,6 +4084,17 @@ $projectCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('resourceType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('statusCounters'), 'type' => Database::VAR_STRING, From 7593537f3d55d7b3fe9909e8c5d48b475a94eaa8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 7 Aug 2024 16:02:24 +0300 Subject: [PATCH 077/241] Add error hook --- composer.lock | 26 ++++++++++---------- src/Appwrite/Platform/Workers/Migrations.php | 4 ++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/composer.lock b/composer.lock index 6949a53726..a7b43d2011 100644 --- a/composer.lock +++ b/composer.lock @@ -2176,12 +2176,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "ae6151e7a65fc5bf253461cd8db909001d7549e9" + "reference": "274115b3246e40b9235f259288a9bbc3f403227c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/ae6151e7a65fc5bf253461cd8db909001d7549e9", - "reference": "ae6151e7a65fc5bf253461cd8db909001d7549e9", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/274115b3246e40b9235f259288a9bbc3f403227c", + "reference": "274115b3246e40b9235f259288a9bbc3f403227c", "shasum": "" }, "require": { @@ -2224,7 +2224,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-08-06T09:32:45+00:00" + "time": "2024-08-07T12:56:55+00:00" }, { "name": "utopia-php/mongo", @@ -3167,16 +3167,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.1", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/b5b6f716db298671c1dfea5b1082ec2c0ae7064f", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -3187,13 +3187,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -3229,7 +3229,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-01T09:06:33+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "matthiasmullie/minify", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index d80df6b1a0..42e2035964 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -37,7 +37,6 @@ class Migrations extends Action protected Database $dbForConsole; protected Document $project; - //protected Document $migration; public static function getName(): string { @@ -375,6 +374,9 @@ class Migrations extends Action $log->addTag('migrationErrors', json_encode($errorMessages)); } } finally { + $destination->error($migration); + $source->error($migration); + if (! $tempAPIKey->isEmpty()) { $this->removeAPIKey($tempAPIKey); } From 0ac5e2aa76cf19cda99f434162ef2aeb03ce0d79 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 7 Aug 2024 16:33:54 +0300 Subject: [PATCH 078/241] error no params --- composer.lock | 8 ++++---- src/Appwrite/Platform/Workers/Migrations.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index a7b43d2011..ee21f6f98a 100644 --- a/composer.lock +++ b/composer.lock @@ -2176,12 +2176,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "274115b3246e40b9235f259288a9bbc3f403227c" + "reference": "d579f344cea485b6519b3050f41d976c62c3c4ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/274115b3246e40b9235f259288a9bbc3f403227c", - "reference": "274115b3246e40b9235f259288a9bbc3f403227c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/d579f344cea485b6519b3050f41d976c62c3c4ba", + "reference": "d579f344cea485b6519b3050f41d976c62c3c4ba", "shasum": "" }, "require": { @@ -2224,7 +2224,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-08-07T12:56:55+00:00" + "time": "2024-08-07T13:33:05+00:00" }, { "name": "utopia-php/mongo", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 42e2035964..1d252b1bd5 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -374,8 +374,8 @@ class Migrations extends Action $log->addTag('migrationErrors', json_encode($errorMessages)); } } finally { - $destination->error($migration); - $source->error($migration); + $destination->error(); + $source->error(); if (! $tempAPIKey->isEmpty()) { $this->removeAPIKey($tempAPIKey); From f12a578efe419bbfb62ce53cded10388808db9f9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 7 Aug 2024 16:58:00 +0300 Subject: [PATCH 079/241] Update on failed --- src/Appwrite/Platform/Workers/Migrations.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 1d252b1bd5..250f82bcec 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -374,9 +374,6 @@ class Migrations extends Action $log->addTag('migrationErrors', json_encode($errorMessages)); } } finally { - $destination->error(); - $source->error(); - if (! $tempAPIKey->isEmpty()) { $this->removeAPIKey($tempAPIKey); } @@ -384,6 +381,9 @@ class Migrations extends Action $this->updateMigrationDocument($migration, $projectDocument); if ($migration->getAttribute('status', '') == 'failed') { + $destination->error(); + $source->error(); + throw new Exception('Migration failed'); } } From 41beb8af9cc7e424c0965e1a1d470c78ffd56dfa Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 8 Aug 2024 15:14:43 +0300 Subject: [PATCH 080/241] credentials --- src/Appwrite/Platform/Workers/Migrations.php | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 250f82bcec..74bca22206 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -126,7 +126,7 @@ class Migrations extends Action ), SourceAppwrite::getName() => new SourceAppwrite( $credentials['projectId'], - str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], + $credentials['endpoint'], $credentials['apiKey'] ), default => throw new \Exception('Invalid source type'), @@ -136,14 +136,15 @@ class Migrations extends Action /** * @throws Exception */ - protected function processDestination(Document $migration, array $credentials): Destination + protected function processDestination(Document $migration): Destination { $destination = $migration->getAttribute('destination'); + $credentials = $migration->getAttribute('credentials'); return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( $credentials['projectId'], - str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], + $credentials['endpoint'], $credentials['apiKey'], $this->dbForProject, Config::getParam('collections', [])['databases']['collections'], @@ -277,16 +278,19 @@ class Migrations extends Action $log->addTag('type', $migration->getAttribute('source')); - $source = $this->processSource($migration); - - $destination = $this->processDestination( - $migration, - [ + if ( + $migration->getAttribute('source') === SourceAppwrite::getName() || + $migration->getAttribute('destination') === DestinationAppwrite::getName() + ) { + $migration->setAttribute('credentials', [ 'projectId' => $projectDocument->getId(), 'endpoint' => 'http://appwrite/v1', 'apiKey' => $tempAPIKey['secret'], - ] - ); + ]); + } + + $source = $this->processSource($migration); + $destination = $this->processDestination($migration); $source->report(); From d29740f0d7d79b76636cfaa54177d1822d6e4e32 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 8 Aug 2024 15:43:11 +0300 Subject: [PATCH 081/241] credentials --- composer.lock | 12 +++---- src/Appwrite/Platform/Workers/Migrations.php | 37 +++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/composer.lock b/composer.lock index ee21f6f98a..fbf409232b 100644 --- a/composer.lock +++ b/composer.lock @@ -1721,16 +1721,16 @@ }, { "name": "utopia-php/database", - "version": "0.50.2", + "version": "0.50.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa" + "reference": "4287e6625c7273411c7322abd151c4285ee7b50f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", - "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", + "url": "https://api.github.com/repos/utopia-php/database/zipball/4287e6625c7273411c7322abd151c4285ee7b50f", + "reference": "4287e6625c7273411c7322abd151c4285ee7b50f", "shasum": "" }, "require": { @@ -1771,9 +1771,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.50.2" + "source": "https://github.com/utopia-php/database/tree/0.50.3" }, - "time": "2024-07-31T10:12:19+00:00" + "time": "2024-08-08T01:40:54+00:00" }, { "name": "utopia-php/domains", diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 74bca22206..4fbc83d8e8 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -38,6 +38,8 @@ class Migrations extends Action protected Document $project; + protected array $credentials = []; + public static function getName(): string { return 'migrations'; @@ -125,9 +127,9 @@ class Migrations extends Action $credentials['port'], ), SourceAppwrite::getName() => new SourceAppwrite( - $credentials['projectId'], - $credentials['endpoint'], - $credentials['apiKey'] + $this->credentials['projectId'], + $this->credentials['endpoint'], + $this->credentials['apiKey'] ), default => throw new \Exception('Invalid source type'), }; @@ -139,13 +141,12 @@ class Migrations extends Action protected function processDestination(Document $migration): Destination { $destination = $migration->getAttribute('destination'); - $credentials = $migration->getAttribute('credentials'); return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( - $credentials['projectId'], - $credentials['endpoint'], - $credentials['apiKey'], + $this->credentials['projectId'], + $this->credentials['endpoint'], + $this->credentials['apiKey'], $this->dbForProject, Config::getParam('collections', [])['databases']['collections'], ), @@ -278,16 +279,18 @@ class Migrations extends Action $log->addTag('type', $migration->getAttribute('source')); - if ( - $migration->getAttribute('source') === SourceAppwrite::getName() || - $migration->getAttribute('destination') === DestinationAppwrite::getName() - ) { - $migration->setAttribute('credentials', [ - 'projectId' => $projectDocument->getId(), - 'endpoint' => 'http://appwrite/v1', - 'apiKey' => $tempAPIKey['secret'], - ]); - } +// if ( +// $migration->getAttribute('source') === SourceAppwrite::getName() || +// $migration->getAttribute('destination') === DestinationAppwrite::getName() +// ) { +// $migration->setAttribute('credentials', ); +// } + + $this->credentials = [ + 'projectId' => $projectDocument->getId(), + 'endpoint' => 'http://appwrite/v1', + 'apiKey' => $tempAPIKey['secret'], + ]; $source = $this->processSource($migration); $destination = $this->processDestination($migration); From 3800ccee1d62dbd2bb94352e39e32c583e193d7b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 8 Aug 2024 19:46:44 +0300 Subject: [PATCH 082/241] credentials --- src/Appwrite/Platform/Workers/Migrations.php | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 4fbc83d8e8..7cad7f8b5e 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -268,6 +268,13 @@ class Migrations extends Action $project = $this->project; $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); $tempAPIKey = $this->generateAPIKey($projectDocument); + + $this->credentials = [ + 'projectId' => $projectDocument->getId(), + 'endpoint' => 'http://appwrite/v1', + 'apiKey' => $tempAPIKey['secret'], + ]; + $transfer = $source = $destination = null; try { @@ -279,19 +286,6 @@ class Migrations extends Action $log->addTag('type', $migration->getAttribute('source')); -// if ( -// $migration->getAttribute('source') === SourceAppwrite::getName() || -// $migration->getAttribute('destination') === DestinationAppwrite::getName() -// ) { -// $migration->setAttribute('credentials', ); -// } - - $this->credentials = [ - 'projectId' => $projectDocument->getId(), - 'endpoint' => 'http://appwrite/v1', - 'apiKey' => $tempAPIKey['secret'], - ]; - $source = $this->processSource($migration); $destination = $this->processDestination($migration); From 9a140f90d48dbfc998812ec7413af4e836c4b7ce Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Fri, 9 Aug 2024 14:12:10 +0000 Subject: [PATCH 083/241] update image library --- app/console | 1 + composer.json | 2 +- composer.lock | 62 ++++++++++++++++++++++++++------------------------- 3 files changed, 34 insertions(+), 31 deletions(-) create mode 160000 app/console diff --git a/app/console b/app/console new file mode 160000 index 0000000000..412bc33318 --- /dev/null +++ b/app/console @@ -0,0 +1 @@ +Subproject commit 412bc3331891c15ba7baf4f445e82ac3a678c6be diff --git a/composer.json b/composer.json index 3bb2816028..8a87b3be12 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.2.*", - "utopia-php/image": "0.6.*", + "utopia-php/image": "dev-feat-avif-support", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", diff --git a/composer.lock b/composer.lock index 27c11b9af2..fa3519afbf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fc07cbc344782534962fd7bf0769f7b9", + "content-hash": "718915e811d332c864cee267c56a35c8", "packages": [ { "name": "adhocore/jwt", @@ -1721,16 +1721,16 @@ }, { "name": "utopia-php/database", - "version": "0.50.2", + "version": "0.50.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa" + "reference": "4287e6625c7273411c7322abd151c4285ee7b50f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", - "reference": "c712d1f6c8ec37886a7a1ad4d60a8cd75dec00aa", + "url": "https://api.github.com/repos/utopia-php/database/zipball/4287e6625c7273411c7322abd151c4285ee7b50f", + "reference": "4287e6625c7273411c7322abd151c4285ee7b50f", "shasum": "" }, "require": { @@ -1771,9 +1771,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.50.2" + "source": "https://github.com/utopia-php/database/tree/0.50.3" }, - "time": "2024-07-31T10:12:19+00:00" + "time": "2024-08-08T01:40:54+00:00" }, { "name": "utopia-php/domains", @@ -1968,21 +1968,21 @@ }, { "name": "utopia-php/image", - "version": "0.6.1", + "version": "dev-feat-avif-support", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0" + "reference": "7be404a23399dbb06585f8c78ed61a375a090450" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/2d74c27e69e65a93cf94a16586598a04fe435bf0", - "reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0", + "url": "https://api.github.com/repos/utopia-php/image/zipball/7be404a23399dbb06585f8c78ed61a375a090450", + "reference": "7be404a23399dbb06585f8c78ed61a375a090450", "shasum": "" }, "require": { "ext-imagick": "*", - "php": ">=8.0" + "php": ">=8.1" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2010,9 +2010,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/0.6.1" + "source": "https://github.com/utopia-php/image/tree/feat-avif-support" }, - "time": "2024-02-05T13:31:44+00:00" + "time": "2024-07-29T13:22:58+00:00" }, { "name": "utopia-php/locale", @@ -2990,16 +2990,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.4", + "version": "0.39.6", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "501b92d73ae55e0f880ed00f57bc64a54d0ce137" + "reference": "00e6f9e77ea380d8ab3138a36548b353077e4061" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/501b92d73ae55e0f880ed00f57bc64a54d0ce137", - "reference": "501b92d73ae55e0f880ed00f57bc64a54d0ce137", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/00e6f9e77ea380d8ab3138a36548b353077e4061", + "reference": "00e6f9e77ea380d8ab3138a36548b353077e4061", "shasum": "" }, "require": { @@ -3035,9 +3035,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.39.4" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.6" }, - "time": "2024-07-26T22:34:10+00:00" + "time": "2024-08-08T12:44:28+00:00" }, { "name": "doctrine/deprecations", @@ -3158,16 +3158,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.1", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/b5b6f716db298671c1dfea5b1082ec2c0ae7064f", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -3178,13 +3178,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -3220,7 +3220,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-01T09:06:33+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "matthiasmullie/minify", @@ -5593,7 +5593,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/image": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From 919b5ac06ded738afa697f9258f38f7d582526bc Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 12 Aug 2024 11:15:14 +0300 Subject: [PATCH 084/241] Use document credentials --- src/Appwrite/Platform/Workers/Migrations.php | 41 +++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 7cad7f8b5e..1ae127dc03 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -38,7 +38,7 @@ class Migrations extends Action protected Document $project; - protected array $credentials = []; + //protected array $credentials = []; public static function getName(): string { @@ -127,9 +127,9 @@ class Migrations extends Action $credentials['port'], ), SourceAppwrite::getName() => new SourceAppwrite( - $this->credentials['projectId'], - $this->credentials['endpoint'], - $this->credentials['apiKey'] + $credentials['projectId'], + $credentials['endpoint'], + $credentials['apiKey'], ), default => throw new \Exception('Invalid source type'), }; @@ -141,12 +141,13 @@ class Migrations extends Action protected function processDestination(Document $migration): Destination { $destination = $migration->getAttribute('destination'); + $credentials = $migration->getAttribute('credentials'); return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( - $this->credentials['projectId'], - $this->credentials['endpoint'], - $this->credentials['apiKey'], + $credentials['projectId'], + $credentials['endpoint'], + $credentials['apiKey'], $this->dbForProject, Config::getParam('collections', [])['databases']['collections'], ), @@ -269,16 +270,30 @@ class Migrations extends Action $projectDocument = $this->dbForConsole->getDocument('projects', $project->getId()); $tempAPIKey = $this->generateAPIKey($projectDocument); - $this->credentials = [ - 'projectId' => $projectDocument->getId(), - 'endpoint' => 'http://appwrite/v1', - 'apiKey' => $tempAPIKey['secret'], - ]; - + // $this->credentials = [ + // 'projectId' => $projectDocument->getId(), + // 'endpoint' => 'http://appwrite/v1', + // 'apiKey' => $tempAPIKey['secret'], + // ]; + $transfer = $source = $destination = null; try { $migration = $this->dbForProject->getDocument('migrations', $migration->getId()); + + if ( + $migration->getAttribute('source') === SourceAppwrite::getName() || + $migration->getAttribute('destination') === DestinationAppwrite::getName() + ) { + $credentials = $migration->getAttribute('credentials', []); + + $credentials['projectId'] = $credentials['projectId'] ?? $projectDocument->getId(); + $credentials['endpoint'] = $credentials['endpoint'] ?? 'http://appwrite/v1'; + $credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey['secret']; + + $migration->setAttribute('credentials', $credentials); + } + $migration->setAttribute('stage', 'processing'); $migration->setAttribute('status', 'processing'); $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'processing'", \microtime(true))); From ab4ac2e56847209035dd3766506bddaa647207c1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 12 Aug 2024 17:44:05 +0300 Subject: [PATCH 085/241] Change private to protected --- src/Appwrite/Platform/Workers/Deletes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index d54f3f5079..cc366c7877 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -951,7 +951,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void + protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void { $count = 0; $chunk = 0; @@ -993,7 +993,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void + protected function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void { $count = 0; $chunk = 0; From ccc34b049ad6cd011510b9facaca12ca0689011c Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 14:03:49 +0300 Subject: [PATCH 086/241] notifyProjects --- src/Appwrite/Platform/Tasks/Maintenance.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index cfbc4b200a..cd1c40cbe8 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -49,11 +49,7 @@ class Maintenance extends Action $this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) { $queueForDeletes->setProject($project); - $this->notifyDeleteExecutionLogs($queueForDeletes); - $this->notifyDeleteAbuseLogs($queueForDeletes); - $this->notifyDeleteAuditLogs($queueForDeletes); - $this->notifyDeleteUsageStats($usageStatsRetentionHourly, $queueForDeletes); - $this->notifyDeleteExpiredSessions($queueForDeletes); + $this->notifyProjects($queueForDeletes, $usageStatsRetentionHourly); }); $this->notifyDeleteConnections($queueForDeletes); @@ -64,6 +60,15 @@ class Maintenance extends Action }, $interval, $delay); } + protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void + { + $this->notifyDeleteExecutionLogs($queueForDeletes); + $this->notifyDeleteAbuseLogs($queueForDeletes); + $this->notifyDeleteAuditLogs($queueForDeletes); + $this->notifyDeleteUsageStats($usageStatsRetentionHourly, $queueForDeletes); + $this->notifyDeleteExpiredSessions($queueForDeletes); + } + protected function foreachProject(Database $dbForConsole, callable $callback): void { // TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document From 9265ba743e6ab571d247f78b7da971091da240ee Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 15:19:06 +0300 Subject: [PATCH 087/241] Add implements Projects --- src/Appwrite/Platform/Tasks/Maintenance.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index cd1c40cbe8..7f8a907d8b 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -12,7 +12,12 @@ use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\System\System; -class Maintenance extends Action +interface Projects +{ + public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); +} + +class Maintenance extends Action implements Projects { public static function getName(): string { @@ -60,7 +65,7 @@ class Maintenance extends Action }, $interval, $delay); } - protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void + public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { $this->notifyDeleteExecutionLogs($queueForDeletes); $this->notifyDeleteAbuseLogs($queueForDeletes); @@ -188,4 +193,14 @@ class Maintenance extends Action ->setType(DELETE_TYPE_EXPIRED_TARGETS) ->trigger(); } + + public function setVariable($name, $var) + { + // TODO: Implement setVariable() method. + } + + public function getHtml($template) + { + // TODO: Implement getHtml() method. + } } From aa5ce6200de649b366f74a0b8c167faf56ca2863 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 16:33:15 +0300 Subject: [PATCH 088/241] Appwrite notifyProjects --- src/Appwrite/Platform/Tasks/Maintenance.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 7f8a907d8b..bf691b1e83 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -67,6 +67,8 @@ class Maintenance extends Action implements Projects public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { + Console::info("Appwrite notifyProjects"); + $this->notifyDeleteExecutionLogs($queueForDeletes); $this->notifyDeleteAbuseLogs($queueForDeletes); $this->notifyDeleteAuditLogs($queueForDeletes); From 49b659a9aed661bd53d297b12d4e25ffc0d3f29c Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 18:01:29 +0300 Subject: [PATCH 089/241] Remove interface --- src/Appwrite/Platform/Tasks/Maintenance.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index bf691b1e83..629a6ab11f 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -12,12 +12,12 @@ use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\System\System; -interface Projects -{ - public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); -} +//interface Projects +//{ +// public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); +//} -class Maintenance extends Action implements Projects +class Maintenance extends Action { public static function getName(): string { @@ -51,12 +51,16 @@ class Maintenance extends Action implements Projects Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); + var_dump('shmuel 1'); $this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) { $queueForDeletes->setProject($project); + var_dump('shmuel 2'); $this->notifyProjects($queueForDeletes, $usageStatsRetentionHourly); + var_dump('shmuel 3'); }); + var_dump('shmuel 4'); $this->notifyDeleteConnections($queueForDeletes); $this->renewCertificates($dbForConsole, $queueForCertificates); $this->notifyDeleteCache($cacheRetention, $queueForDeletes); From b47fd08f712350c4416e1733b796c7e50c6dc141 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 18:26:54 +0300 Subject: [PATCH 090/241] Remove interface --- src/Appwrite/Platform/Tasks/Maintenance.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 629a6ab11f..d967ead5b4 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -82,6 +82,9 @@ class Maintenance extends Action protected function foreachProject(Database $dbForConsole, callable $callback): void { + + var_dump('shmuel 22'); + // TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document $count = 0; $chunk = 0; @@ -90,13 +93,14 @@ class Maintenance extends Action $executionStart = \microtime(true); while ($sum === $limit) { + var_dump('shmuel 33'); $projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]); $chunk++; /** @var string[] $projectIds */ $sum = count($projects); - + var_dump('shmuel 44'); foreach ($projects as $project) { $callback($project); $count++; From 04967763e762ddf28e46fc489feb1f01971bf90c Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 19 Aug 2024 18:46:20 +0300 Subject: [PATCH 091/241] Add Iprojects --- src/Appwrite/Platform/Tasks/Maintenance.php | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index d967ead5b4..aaf8b589da 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -12,12 +12,12 @@ use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\System\System; -//interface Projects -//{ -// public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); -//} +interface Iprojects +{ + public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); +} -class Maintenance extends Action +class Maintenance extends Action implements Iprojects { public static function getName(): string { @@ -51,16 +51,12 @@ class Maintenance extends Action Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); - var_dump('shmuel 1'); $this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) { $queueForDeletes->setProject($project); - var_dump('shmuel 2'); $this->notifyProjects($queueForDeletes, $usageStatsRetentionHourly); - var_dump('shmuel 3'); }); - var_dump('shmuel 4'); $this->notifyDeleteConnections($queueForDeletes); $this->renewCertificates($dbForConsole, $queueForCertificates); $this->notifyDeleteCache($cacheRetention, $queueForDeletes); @@ -82,9 +78,6 @@ class Maintenance extends Action protected function foreachProject(Database $dbForConsole, callable $callback): void { - - var_dump('shmuel 22'); - // TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document $count = 0; $chunk = 0; @@ -93,14 +86,12 @@ class Maintenance extends Action $executionStart = \microtime(true); while ($sum === $limit) { - var_dump('shmuel 33'); $projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]); $chunk++; /** @var string[] $projectIds */ $sum = count($projects); - var_dump('shmuel 44'); foreach ($projects as $project) { $callback($project); $count++; From 42671400bb97a0622ea9846db768a483b5795c07 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 20 Aug 2024 10:08:07 +0300 Subject: [PATCH 092/241] Remove interface --- src/Appwrite/Platform/Tasks/Maintenance.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index aaf8b589da..748ba1f87d 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -12,12 +12,7 @@ use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\System\System; -interface Iprojects -{ - public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly); -} - -class Maintenance extends Action implements Iprojects +class Maintenance extends Action { public static function getName(): string { @@ -65,9 +60,16 @@ class Maintenance extends Action implements Iprojects }, $interval, $delay); } - public function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void + /** + * Overridable method for Cloud, Please do not delete + * + * @param Delete $queueForDeletes + * @param int $usageStatsRetentionHourly + * @return void + */ + protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { - Console::info("Appwrite notifyProjects"); + var_dump("Appwrite notifyProjects"); $this->notifyDeleteExecutionLogs($queueForDeletes); $this->notifyDeleteAbuseLogs($queueForDeletes); From 9b20802b4ca608ee46c6e6438b161f9fea838621 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 20 Aug 2024 10:09:02 +0300 Subject: [PATCH 093/241] Only comment --- src/Appwrite/Platform/Tasks/Maintenance.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 748ba1f87d..fe488fdbda 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -62,10 +62,6 @@ class Maintenance extends Action /** * Overridable method for Cloud, Please do not delete - * - * @param Delete $queueForDeletes - * @param int $usageStatsRetentionHourly - * @return void */ protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { From 2d84816bc91b42d580a3b3f372d5a655760fc556 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 20 Aug 2024 16:39:39 +0300 Subject: [PATCH 094/241] composer.lock --- composer.lock | 116 +++++++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/composer.lock b/composer.lock index cd1cdf2a11..4127580d05 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5c0db330bf30bd1d240b96116d96baf", + "content-hash": "3582bf9477a65a24fe8030215075ccd4", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -1720,16 +1721,16 @@ }, { "name": "utopia-php/database", - "version": "0.50.1", + "version": "0.50.4", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "1745147bef29a9bddf5dd03fd9174ec29e2c26f0" + "reference": "fd3b856be77bd643bc8a9e3572ee11e4185b9230" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/1745147bef29a9bddf5dd03fd9174ec29e2c26f0", - "reference": "1745147bef29a9bddf5dd03fd9174ec29e2c26f0", + "url": "https://api.github.com/repos/utopia-php/database/zipball/fd3b856be77bd643bc8a9e3572ee11e4185b9230", + "reference": "fd3b856be77bd643bc8a9e3572ee11e4185b9230", "shasum": "" }, "require": { @@ -1770,9 +1771,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.50.1" + "source": "https://github.com/utopia-php/database/tree/0.50.4" }, - "time": "2024-07-26T11:56:05+00:00" + "time": "2024-08-13T03:18:26+00:00" }, { "name": "utopia-php/domains", @@ -1922,16 +1923,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.6", + "version": "0.33.8", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6" + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", + "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", "shasum": "" }, "require": { @@ -1961,9 +1962,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.6" + "source": "https://github.com/utopia-php/http/tree/0.33.8" }, - "time": "2024-03-21T18:10:57+00:00" + "time": "2024-08-15T14:10:09+00:00" }, { "name": "utopia-php/image", @@ -2171,27 +2172,35 @@ }, { "name": "utopia-php/migration", - "version": "0.5.2", + "version": "dev-backups", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a" + "reference": "2bb9c1bf89eff0e90e47f3844cb43eda80e6a069" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f18d44d4459f78c292dac9edde856fd156fe497a", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/2bb9c1bf89eff0e90e47f3844cb43eda80e6a069", + "reference": "2bb9c1bf89eff0e90e47f3844cb43eda80e6a069", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "11.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3", + "utopia-php/database": "0.50.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "0.33.*", + "utopia-php/storage": "0.18.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", - "vlucas/phpdotenv": "5.*" + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.16.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2213,9 +2222,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.2" + "source": "https://github.com/utopia-php/migration/tree/backups" }, - "time": "2024-07-22T09:27:07+00:00" + "time": "2024-08-20T13:02:03+00:00" }, { "name": "utopia-php/mongo", @@ -2758,16 +2767,16 @@ }, { "name": "utopia-php/vcs", - "version": "0.8.1", + "version": "0.8.2", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "3084aa93d24ed1e70f01e75f4318fc0d07f12596" + "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/3084aa93d24ed1e70f01e75f4318fc0d07f12596", - "reference": "3084aa93d24ed1e70f01e75f4318fc0d07f12596", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", + "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", "shasum": "" }, "require": { @@ -2801,9 +2810,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.8.1" + "source": "https://github.com/utopia-php/vcs/tree/0.8.2" }, - "time": "2024-07-29T20:49:09+00:00" + "time": "2024-08-13T14:36:30+00:00" }, { "name": "utopia-php/websocket", @@ -3158,16 +3167,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.0", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -3178,13 +3187,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -3220,7 +3229,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-07-23T16:40:20+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "matthiasmullie/minify", @@ -5591,9 +5600,18 @@ "time": "2023-11-21T18:54:41+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/migration", + "version": "dev-backups", + "alias": "0.4.999", + "alias_normalized": "0.4.999.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From f1da0b2e613c07479dfd179bff1072263e52cfc0 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Thu, 22 Aug 2024 17:21:11 +0300 Subject: [PATCH 095/241] Update src/Appwrite/Platform/Tasks/Maintenance.php Co-authored-by: Jake Barnby --- src/Appwrite/Platform/Tasks/Maintenance.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 8421d9291c..3f06fe42a2 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -60,7 +60,7 @@ class Maintenance extends Action } /** - * Overridable method for Cloud, Please do not delete + * Hook to allow sub-classes to extend project-level maintenance functionality. */ protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { From 5f27d75c11d921b86cd26f97426d8d28416c9a1d Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 17:30:50 +0300 Subject: [PATCH 096/241] Address comments --- app/controllers/api/databases.php | 8 +-- src/Appwrite/Platform/Tasks/Maintenance.php | 2 - src/Appwrite/Platform/Tasks/ScheduleBase.php | 9 +-- .../e2e/Services/Databases/DatabasesBase.php | 56 +------------------ 4 files changed, 3 insertions(+), 72 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 6740325261..2643a79f54 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2743,17 +2743,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('data', [], new JSON(), 'Document data as JSON object.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->inject('response') - ->inject('request') ->inject('dbForProject') ->inject('user') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Request $request, Database $dbForProject, Document $user, Event $queueForEvents, string $mode) { - - if ($request->getHeader('x-appwrite-preserve-dates') === 'true') { - $dbForProject->setPreserveDates(true); - } - + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data)) { diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index 8421d9291c..167e837ddb 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -64,8 +64,6 @@ class Maintenance extends Action */ protected function notifyProjects(Delete $queueForDeletes, int $usageStatsRetentionHourly): void { - var_dump("Appwrite notifyProjects"); - $this->notifyDeleteTargets($queueForDeletes); $this->notifyDeleteExecutionLogs($queueForDeletes); $this->notifyDeleteAbuseLogs($queueForDeletes); diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 3904f94fb9..f5e4d92612 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,10 +59,6 @@ abstract class ScheduleBase extends Action $getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array { $project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId')); - var_dump('===== $getSchedule getCollectionId = '); - var_dump(static::getCollectionId()); - var_dump('===== $getSchedule getCollectionId = '); - $resource = $getProjectDB($project)->getDocument( static::getCollectionId(), $schedule->getAttribute('resourceId') @@ -105,10 +101,7 @@ abstract class ScheduleBase extends Action foreach ($results as $document) { try { - var_dump('=== ScheduleBase start'); - var_dump($getSchedule($document)['resource']); - var_dump('=== ScheduleBase end'); - //todo: use a unique key as InternalId or add projectId + //todo: add projectId to be unique $this->schedules[$document['resourceId']]['projectId'] $this->schedules[$document['resourceId']] = $getSchedule($document); } catch (\Throwable $th) { $collectionId = static::getCollectionId(); diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 1b0bd45a8a..6f65552f1c 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -4754,8 +4754,6 @@ trait DatabasesBase $this->assertEquals($longtext['headers']['status-code'], 202); - $longtext = file_get_contents(__DIR__ . '/../../../resources/longtext.txt'); - for ($i = 0; $i < 1; $i++) { $this->client->call(Client::METHOD_POST, '/databases/' . $data['databaseId'] . '/collections/' . $data['$id'] . '/documents', array_merge([ 'content-type' => 'application/json', @@ -4763,7 +4761,7 @@ trait DatabasesBase ], $this->getHeaders()), [ 'documentId' => ID::unique(), 'data' => [ - 'longtext' => $longtext . $longtext, + 'longtext' => file_get_contents(__DIR__ . '/../../../resources/longtext.txt'), ], 'permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), @@ -4785,56 +4783,4 @@ trait DatabasesBase $this->assertEquals(408, $response['headers']['status-code']); } - - /** - * @depends testCreateDatabase - */ - public function testPreserveDates(array $data): void - { - $databaseId = $data['databaseId']; - - $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'collectionId' => 'preserve_dates', - 'name' => 'Preserve Dates', - 'documentSecurity' => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - ], - ]); - - $this->assertEquals(201, $collection['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection['body']['$id'] . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-preserve-dates' => 'true' - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - '$createdAt' => '2000-01-01 01:01:01', - '$updatedAt' => '2020-01-01 01:01:01', - ], - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - ] - ]); - $this->assertEquals(201, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collection['body']['$id'] . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('$id', [$response['body']['$id']])->toString(), - ], - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('2000-01-01T01:01:01.000+00:00', $response['body']['documents'][0]['$createdAt']); - $this->assertEquals('2020-01-01T01:01:01.000+00:00', $response['body']['documents'][0]['$updatedAt']); - } } From 4b0c78289141f01086df644ca1de925cb20e357d Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:18:56 +0300 Subject: [PATCH 097/241] databases 0.52.* --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 553ac8ca23..3fabbe7a9d 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.50.*", + "utopia-php/database": "0.52.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From fd52a172a79fa455e5f5066c249659d965401b2c Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:20:57 +0300 Subject: [PATCH 098/241] audit abuse --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3fabbe7a9d..dc109a555d 100644 --- a/composer.json +++ b/composer.json @@ -44,9 +44,9 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.38.*", + "utopia-php/abuse": "0.42.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.40.*", + "utopia-php/audit": "0.42.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", From 43e7c1e9a68c4be60a28bad8f7c4769d97238dc1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:36:41 +0300 Subject: [PATCH 099/241] database 50 --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index dc109a555d..f33463e90b 100644 --- a/composer.json +++ b/composer.json @@ -44,13 +44,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.42.*", + "utopia-php/abuse": "0.37.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.42.*", - "utopia-php/cache": "0.10.*", + "utopia-php/audit": "0.39.*", + "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.52.*", + "utopia-php/database": "0.50.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From 1cdac28631580e444667b7f04ce079c5631f868b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:42:22 +0300 Subject: [PATCH 100/241] 0.10.* cache --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f33463e90b..de588ef65f 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "utopia-php/abuse": "0.37.*", "utopia-php/analytics": "0.10.*", "utopia-php/audit": "0.39.*", - "utopia-php/cache": "0.9.*", + "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.50.*", From 9419e36de0ccb9e1292d20369bb80f7cacecf33b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:43:04 +0300 Subject: [PATCH 101/241] abuse --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index de588ef65f..effeb7f3cc 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.37.*", + "utopia-php/abuse": "0.39.*", "utopia-php/analytics": "0.10.*", "utopia-php/audit": "0.39.*", "utopia-php/cache": "0.10.*", From cf7e1121fa66a6a8da48df71c1e36e678fb340cd Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:43:42 +0300 Subject: [PATCH 102/241] audit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index effeb7f3cc..06d9f00579 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "appwrite/php-clamav": "2.0.*", "utopia-php/abuse": "0.39.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.39.*", + "utopia-php/audit": "0.40.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", From 404ad5b75dfafff7216f4672398ac3d0edba0f2c Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:52:06 +0300 Subject: [PATCH 103/241] database 49 --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 06d9f00579..0ce7892427 100644 --- a/composer.json +++ b/composer.json @@ -44,13 +44,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.39.*", + "utopia-php/abuse": "0.37.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.40.*", - "utopia-php/cache": "0.10.*", + "utopia-php/audit": "0.39.*", + "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.50.*", + "utopia-php/database": "0.49.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From b0e218cb3b9de8e4aa1401a926b66d13833c970e Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 18:58:16 +0300 Subject: [PATCH 104/241] database 50 --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 0ce7892427..553ac8ca23 100644 --- a/composer.json +++ b/composer.json @@ -44,13 +44,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.37.*", + "utopia-php/abuse": "0.38.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.39.*", - "utopia-php/cache": "0.9.*", + "utopia-php/audit": "0.40.*", + "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.49.*", + "utopia-php/database": "0.50.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From 2fc483b9e05b3971f7b1aaefc69a2a6f7911d5e6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 19:01:19 +0300 Subject: [PATCH 105/241] rm migration version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 553ac8ca23..0f263a2594 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-backups as 0.4.999", + "utopia-php/migration": "dev-backups", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From ed0652f86a355aac1c130cde42f79dbd683552e9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 22 Aug 2024 19:11:06 +0300 Subject: [PATCH 106/241] database 52 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 0f263a2594..4af025fefe 100644 --- a/composer.json +++ b/composer.json @@ -44,13 +44,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.13.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.38.*", + "utopia-php/abuse": "0.42.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.40.*", + "utopia-php/audit": "0.42.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.50.*", + "utopia-php/database": "0.52.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", From 732a8938ce42daff1abac09fb5cb231e001c3e99 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 25 Aug 2024 09:48:50 +0300 Subject: [PATCH 107/241] Update composer.json Co-authored-by: Jake Barnby --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4af025fefe..73bb84159a 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.5.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-backups", + "utopia-php/migration": "0.6.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From b35f90638445465baf794c7b44c186aac5d554db Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 25 Aug 2024 09:49:44 +0300 Subject: [PATCH 108/241] Longtext test --- tests/e2e/Services/Databases/DatabasesBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 6f65552f1c..6ebd95aa5d 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -4754,7 +4754,7 @@ trait DatabasesBase $this->assertEquals($longtext['headers']['status-code'], 202); - for ($i = 0; $i < 1; $i++) { + for ($i = 0; $i < 10; $i++) { $this->client->call(Client::METHOD_POST, '/databases/' . $data['databaseId'] . '/collections/' . $data['$id'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From e11e92258727d0817aa59e47a529a94dc890a50e Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 25 Aug 2024 09:58:54 +0300 Subject: [PATCH 109/241] Move scopes --- app/config/scopes.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/app/config/scopes.php b/app/config/scopes.php index 99faa59e54..3765ab54fa 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -130,22 +130,4 @@ return [ // List of publicly visible scopes 'assistant.read' => [ 'description' => 'Access to read the Assistant service', ], - 'policies.write' => [ - 'description' => 'Access to create, update, and delete your backups', - ], - 'policies.read' => [ - 'description' => 'Access to read the backups service', - ], - 'archives.read' => [ - 'description' => 'Access to read the backups service', - ], - 'archives.write' => [ - 'description' => 'Access to create, update, and delete your archive', - ], - 'restorations.read' => [ - 'description' => 'Access to read your restorations', - ], - 'restorations.write' => [ - 'description' => 'Access to create, update, and delete your restoration', - ], ]; From 6b1196fb26d52451e97b98d5490677a1f03e60cd Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 25 Aug 2024 11:10:42 +0300 Subject: [PATCH 110/241] update composer.lock --- composer.lock | 135 ++++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/composer.lock b/composer.lock index 4127580d05..a0d2f3be1f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3582bf9477a65a24fe8030215075ccd4", + "content-hash": "b737874b23ae7480c9579c5ee28ad46f", "packages": [ { "name": "adhocore/jwt", @@ -1429,26 +1429,28 @@ }, { "name": "utopia-php/abuse", - "version": "0.38.0", + "version": "0.42.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "b7be9086c9d9b4561d810cbd42fdda798742f56c" + "reference": "08cf17e7f4fd213966c8d8702e406f2269244f0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/b7be9086c9d9b4561d810cbd42fdda798742f56c", - "reference": "b7be9086c9d9b4561d810cbd42fdda798742f56c", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/08cf17e7f4fd213966c8d8702e406f2269244f0f", + "reference": "08cf17e7f4fd213966c8d8702e406f2269244f0f", "shasum": "" }, "require": { "ext-curl": "*", "ext-pdo": "*", + "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.50.*" + "utopia-php/database": "0.52.*" }, "require-dev": { "laravel/pint": "1.5.*", + "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^9.4" }, @@ -1472,9 +1474,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.38.0" + "source": "https://github.com/utopia-php/abuse/tree/0.42.0" }, - "time": "2024-06-24T00:52:02+00:00" + "time": "2024-08-21T08:24:01+00:00" }, { "name": "utopia-php/analytics", @@ -1524,21 +1526,21 @@ }, { "name": "utopia-php/audit", - "version": "0.40.0", + "version": "0.42.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "735ae211ce5fee5b52b736731571b4030b1d7cdc" + "reference": "9dc168470625bcf11ff8cd9ab5660db09129f618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/735ae211ce5fee5b52b736731571b4030b1d7cdc", - "reference": "735ae211ce5fee5b52b736731571b4030b1d7cdc", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/9dc168470625bcf11ff8cd9ab5660db09129f618", + "reference": "9dc168470625bcf11ff8cd9ab5660db09129f618", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.50.*" + "utopia-php/database": "0.52.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1565,9 +1567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.40.0" + "source": "https://github.com/utopia-php/audit/tree/0.42.0" }, - "time": "2024-06-24T00:52:17+00:00" + "time": "2024-08-21T08:24:08+00:00" }, { "name": "utopia-php/cache", @@ -1721,16 +1723,16 @@ }, { "name": "utopia-php/database", - "version": "0.50.4", + "version": "0.52.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "fd3b856be77bd643bc8a9e3572ee11e4185b9230" + "reference": "d22a316a010699c5ebb4768c1020167c192b9c6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/fd3b856be77bd643bc8a9e3572ee11e4185b9230", - "reference": "fd3b856be77bd643bc8a9e3572ee11e4185b9230", + "url": "https://api.github.com/repos/utopia-php/database/zipball/d22a316a010699c5ebb4768c1020167c192b9c6b", + "reference": "d22a316a010699c5ebb4768c1020167c192b9c6b", "shasum": "" }, "require": { @@ -1742,14 +1744,14 @@ "utopia-php/mongo": "0.3.*" }, "require-dev": { - "fakerphp/faker": "^1.14", - "laravel/pint": "1.13.*", - "pcov/clobber": "^2.0", - "phpstan/phpstan": "1.10.*", - "phpunit/phpunit": "^9.4", - "rregeer/phpunit-coverage-check": "^0.3.1", - "swoole/ide-helper": "4.8.0", - "utopia-php/cli": "^0.14.0" + "fakerphp/faker": "1.23.*", + "laravel/pint": "1.17.*", + "pcov/clobber": "2.0.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "9.6.*", + "rregeer/phpunit-coverage-check": "0.3.*", + "swoole/ide-helper": "5.1.3", + "utopia-php/cli": "0.14.*" }, "type": "library", "autoload": { @@ -1771,9 +1773,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.50.4" + "source": "https://github.com/utopia-php/database/tree/0.52.1" }, - "time": "2024-08-13T03:18:26+00:00" + "time": "2024-08-23T02:43:43+00:00" }, { "name": "utopia-php/domains", @@ -2172,24 +2174,24 @@ }, { "name": "utopia-php/migration", - "version": "dev-backups", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "2bb9c1bf89eff0e90e47f3844cb43eda80e6a069" + "reference": "33d2fedb7e8e60261f552acfcc42c7875bacc9aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/2bb9c1bf89eff0e90e47f3844cb43eda80e6a069", - "reference": "2bb9c1bf89eff0e90e47f3844cb43eda80e6a069", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/33d2fedb7e8e60261f552acfcc42c7875bacc9aa", + "reference": "33d2fedb7e8e60261f552acfcc42c7875bacc9aa", "shasum": "" }, "require": { "appwrite/appwrite": "11.1.*", "ext-curl": "*", "ext-openssl": "*", - "php": "8.3", - "utopia-php/database": "0.50.*", + "php": "8.3.*", + "utopia-php/database": "0.52.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -2222,9 +2224,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/backups" + "source": "https://github.com/utopia-php/migration/tree/0.6.0" }, - "time": "2024-08-20T13:02:03+00:00" + "time": "2024-08-23T03:43:23+00:00" }, { "name": "utopia-php/mongo", @@ -3884,35 +3886,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3921,7 +3923,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -3950,7 +3952,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -3958,7 +3960,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4306,16 +4308,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { @@ -4350,9 +4352,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.1" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { "name": "sebastian/cli-parser", @@ -5600,18 +5602,9 @@ "time": "2023-11-21T18:54:41+00:00" } ], - "aliases": [ - { - "package": "utopia-php/migration", - "version": "dev-backups", - "alias": "0.4.999", - "alias_normalized": "0.4.999.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { From d8148c54913d2d399bd09b1a9074dd87f96721c5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 25 Aug 2024 11:19:25 +0300 Subject: [PATCH 111/241] Change TimeLimit namespace --- app/controllers/api/projects.php | 2 +- app/controllers/shared/api.php | 2 +- app/http.php | 2 +- app/realtime.php | 2 +- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index bc8f372991..b9b8ad4750 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -14,7 +14,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; -use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 2b0013db29..b2f41bbaee 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -16,7 +16,7 @@ use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Abuse\Abuse; -use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\Cache\Adapter\Filesystem; use Utopia\Cache\Cache; diff --git a/app/http.php b/app/http.php index 20a5288fe4..f8319a0ab2 100644 --- a/app/http.php +++ b/app/http.php @@ -9,7 +9,7 @@ use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; use Swoole\Http\Server; use Swoole\Process; -use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\Audit\Audit; use Utopia\CLI\Console; diff --git a/app/realtime.php b/app/realtime.php index 9c3c2b4d6a..b8fdb2cf21 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -13,7 +13,7 @@ use Swoole\Runtime; use Swoole\Table; use Swoole\Timer; use Utopia\Abuse\Abuse; -use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index cc366c7877..0c799abd50 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -7,7 +7,7 @@ use Appwrite\Extend\Exception; use Executor\Executor; use Throwable; use Utopia\Abuse\Abuse; -use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\Audit\Audit; use Utopia\Cache\Adapter\Filesystem; use Utopia\Cache\Cache; From 2d03ce598b8fbe2b281b3624ea53ac10b3ab921a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 28 Aug 2024 18:00:44 +1200 Subject: [PATCH 112/241] Allow overriding request/response types for spec generation --- src/Appwrite/Platform/Tasks/Specs.php | 27 +++++++++++++++++++-------- src/Appwrite/Utopia/Request.php | 6 +++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 114e12ac85..8b5093eeab 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -5,9 +5,11 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Specification\Format\OpenAPI3; use Appwrite\Specification\Format\Swagger2; use Appwrite\Specification\Specification; -use Appwrite\Utopia\Response; +use Appwrite\Utopia\Request as AppwriteRequest; +use Appwrite\Utopia\Response as AppwriteResponse; use Exception; -use Swoole\Http\Response as HttpResponse; +use Swoole\Http\Request as SwooleRequest; +use Swoole\Http\Response as SwooleResponse; use Utopia\App; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; @@ -17,7 +19,8 @@ use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Platform\Action; use Utopia\Registry\Registry; -use Utopia\Request; +use Utopia\Request as UtopiaRequest; +use Utopia\Response as UtopiaResponse; use Utopia\System\System; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -29,6 +32,16 @@ class Specs extends Action return 'specs'; } + public function getRequest(): UtopiaRequest + { + return new AppwriteRequest(new SwooleRequest()); + } + + public function getResponse(): UtopiaResponse + { + return new AppwriteResponse(new SwooleResponse()); + } + public function __construct() { $this @@ -42,11 +55,11 @@ class Specs extends Action public function action(string $version, string $mode, Registry $register): void { $appRoutes = App::getRoutes(); - $response = new Response(new HttpResponse()); + $response = $this->getResponse(); $mocks = ($mode === 'mocks'); // Mock dependencies - App::setResource('request', fn () => new Request()); + App::setResource('request', fn () => $this->getRequest()); App::setResource('response', fn () => $response); App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None()))); App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); @@ -183,10 +196,8 @@ class Specs extends Action case APP_AUTH_TYPE_SESSION: $sdkPlatforms[] = APP_PLATFORM_CLIENT; break; - case APP_AUTH_TYPE_KEY: - $sdkPlatforms[] = APP_PLATFORM_SERVER; - break; case APP_AUTH_TYPE_JWT: + case APP_AUTH_TYPE_KEY: $sdkPlatforms[] = APP_PLATFORM_SERVER; break; case APP_AUTH_TYPE_ADMIN: diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 3f0a196d5e..26c1baf188 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -122,7 +122,11 @@ class Request extends UtopiaRequest */ public function getHeaders(): array { - $headers = $this->generateHeaders(); + try { + $headers = $this->generateHeaders(); + } catch (\Throwable) { + $headers = []; + } if (empty($this->swoole->cookie)) { return $headers; From c868c669caa29605a152d314875d8af958750e20 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 28 Aug 2024 21:35:16 +1200 Subject: [PATCH 113/241] Move const to init to avoid redefine warnings --- app/config/platforms.php | 4 ---- app/init.php | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index 21db108624..13c6d73203 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -1,9 +1,5 @@ [ 'key' => APP_PLATFORM_CLIENT, diff --git a/app/init.php b/app/init.php index 1eb7cc93ce..45620a9a02 100644 --- a/app/init.php +++ b/app/init.php @@ -142,6 +142,9 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite'; const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite'; const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1'; const APP_HOSTNAME_INTERNAL = 'appwrite'; +const APP_PLATFORM_SERVER = 'server'; +const APP_PLATFORM_CLIENT = 'client'; +const APP_PLATFORM_CONSOLE = 'console'; // Database Reconnect const DATABASE_RECONNECT_SLEEP = 2; From f3a5572b598b27bc73069aa5fbd5de919d97cec5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 28 Aug 2024 22:27:00 +1200 Subject: [PATCH 114/241] Update SDK generator --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 73bb84159a..658ddfc53c 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,7 @@ }, "require-dev": { "ext-fileinfo": "*", - "appwrite/sdk-generator": "0.38.*", + "appwrite/sdk-generator": "0.39.*", "phpunit/phpunit": "9.5.20", "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.7", diff --git a/composer.lock b/composer.lock index a0d2f3be1f..6e9d369fa8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b737874b23ae7480c9579c5ee28ad46f", + "content-hash": "0e770d4489dbe657c00281b1afd3c0ee", "packages": [ { "name": "adhocore/jwt", @@ -3001,16 +3001,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.38.8", + "version": "0.39.18", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef" + "reference": "3f12532d3a41f6e3e7528f41c0e79a6d473c4a4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", - "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/3f12532d3a41f6e3e7528f41c0e79a6d473c4a4c", + "reference": "3f12532d3a41f6e3e7528f41c0e79a6d473c4a4c", "shasum": "" }, "require": { @@ -3046,9 +3046,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.38.8" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.18" }, - "time": "2024-06-17T00:42:27+00:00" + "time": "2024-08-27T11:22:14+00:00" }, { "name": "doctrine/deprecations", From 79fd72ead493f4d6f7bf99bbc76a306220ffe5ba Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Sep 2024 13:13:06 +0300 Subject: [PATCH 115/241] Add backups scopes --- app/config/roles.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/config/roles.php b/app/config/roles.php index 65b9643b89..742212ff86 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -75,7 +75,13 @@ $admins = [ 'topics.write', 'topics.read', 'subscribers.write', - 'subscribers.read' + 'subscribers.read', + 'policies.write', + 'policies.read', + 'archives.read', + 'archives.write', + 'restorations.read', + 'restorations.write', ]; return [ From 209c610cbe91903e6b4512caf5a2dd48dd4ba938 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Sep 2024 13:15:51 +0300 Subject: [PATCH 116/241] composer.lock --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 6e9d369fa8..5b4c1a9546 100644 --- a/composer.lock +++ b/composer.lock @@ -3839,16 +3839,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.30.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", "shasum": "" }, "require": { @@ -3880,9 +3880,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-08-29T09:54:52+00:00" }, { "name": "phpunit/php-code-coverage", From 964be49c3a092c638fd0151744d973a03ecfffdd Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 2 Sep 2024 10:26:03 +0300 Subject: [PATCH 117/241] revert roles.php --- app/config/roles.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index 742212ff86..333ad7e914 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -76,12 +76,6 @@ $admins = [ 'topics.read', 'subscribers.write', 'subscribers.read', - 'policies.write', - 'policies.read', - 'archives.read', - 'archives.write', - 'restorations.read', - 'restorations.write', ]; return [ From a93fa2cb0cbf3985f17c9f177ccc97817ff8fc6f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 2 Sep 2024 21:43:44 +1200 Subject: [PATCH 118/241] Check base class for query validator descendants --- .../Specification/Format/Swagger2.php | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 03dcd38814..c7278a793c 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -286,7 +286,21 @@ class Swagger2 extends Format $validator = $validator->getValidator(); } - switch ((!empty($validator)) ? \get_class($validator) : '') { + $class = !empty($validator) + ? \get_class($validator) + : ''; + + $base = !empty($class) + ? \get_parent_class($class) + : ''; + + switch ($base) { + case 'Appwrite\Utopia\Database\Validator\Queries\Base': + $class = $base; + break; + } + + switch ($class) { case 'Utopia\Validator\Text': case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); @@ -338,29 +352,7 @@ class Swagger2 extends Format $consumes = ['multipart/form-data']; $node['type'] = 'file'; break; - case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': - case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': - case 'Appwrite\Utopia\Database\Validator\Queries\Collections': - case 'Appwrite\Utopia\Database\Validator\Queries\Databases': - case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': - case 'Appwrite\Utopia\Database\Validator\Queries\Executions': - case 'Appwrite\Utopia\Database\Validator\Queries\Files': - case 'Appwrite\Utopia\Database\Validator\Queries\Functions': - case 'Appwrite\Utopia\Database\Validator\Queries\Identities': - case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': - case 'Appwrite\Utopia\Database\Validator\Queries\Installations': - case 'Appwrite\Utopia\Database\Validator\Queries\Memberships': - case 'Appwrite\Utopia\Database\Validator\Queries\Messages': - case 'Appwrite\Utopia\Database\Validator\Queries\Migrations': - case 'Appwrite\Utopia\Database\Validator\Queries\Projects': - case 'Appwrite\Utopia\Database\Validator\Queries\Providers': - case 'Appwrite\Utopia\Database\Validator\Queries\Rules': - case 'Appwrite\Utopia\Database\Validator\Queries\Subscribers': - case 'Appwrite\Utopia\Database\Validator\Queries\Targets': - case 'Appwrite\Utopia\Database\Validator\Queries\Teams': - case 'Appwrite\Utopia\Database\Validator\Queries\Topics': - case 'Appwrite\Utopia\Database\Validator\Queries\Users': - case 'Appwrite\Utopia\Database\Validator\Queries\Variables': + case 'Appwrite\Utopia\Database\Validator\Queries\Base': case 'Utopia\Database\Validator\Queries': case 'Utopia\Database\Validator\Queries\Document': case 'Utopia\Database\Validator\Queries\Documents': From 982ccf8873f2d3389a00b29c7a063856c02359eb Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 2 Sep 2024 14:20:02 +0300 Subject: [PATCH 119/241] logger update --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 658ddfc53c..495ec6221a 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "utopia-php/fetch": "0.2.*", "utopia-php/image": "0.6.*", "utopia-php/locale": "0.4.*", - "utopia-php/logger": "0.5.*", + "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", "utopia-php/migration": "0.6.*", "utopia-php/orchestration": "0.9.*", From 6854451565af4630a9cc4e660d289a36e2e1f547 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 3 Sep 2024 15:07:53 +1200 Subject: [PATCH 120/241] Fix execution scheduler merge --- src/Appwrite/Platform/Tasks/ScheduleExecutions.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index c5f9b40d15..1ac07fc582 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -22,7 +22,12 @@ class ScheduleExecutions extends ScheduleBase return 'execution'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + public static function getCollectionId(): string + { + return 'executions'; + } + + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { $queue = $pools->get('queue')->pop(); $connection = $queue->getResource(); From e79e2857f9122be7c0cd0183586fa61a6ddf69b5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 4 Sep 2024 20:27:29 +1200 Subject: [PATCH 121/241] Remove console --- app/console | 1 - 1 file changed, 1 deletion(-) delete mode 160000 app/console diff --git a/app/console b/app/console deleted file mode 160000 index 0959b594b3..0000000000 --- a/app/console +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0959b594b32f176819d4afb3a769afea212db789 From 1ce6a8502681566de72c33b25862ccf37bdf440d Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 17:25:41 +0300 Subject: [PATCH 122/241] Use preserveDates branch --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 12020477d3..3ecf645817 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.6.*", + "utopia-php/migration": "dev-preserveDates as 0.6.0", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From 745968ebd407bbd98bac7f88894564f0dc133a37 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 19:06:19 +0300 Subject: [PATCH 123/241] Debug info --- src/Appwrite/Platform/Workers/Migrations.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index d6a0bfc583..72f691f1b4 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -332,6 +332,11 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { /** @var $sourceErrors $error */ + var_dump($error->getPrevious()->getMessage()); + var_dump($error->getPrevious()->getLine()); + var_dump($error->getPrevious()->getFile()); + var_dump($error->getLine()); + var_dump($error->getFile()); $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { From 626b2390e4d3ec4274a5182c763c82d5ee6be6d9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 19:06:48 +0300 Subject: [PATCH 124/241] Debug info --- src/Appwrite/Platform/Workers/Migrations.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 72f691f1b4..d4ce30b010 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -340,6 +340,11 @@ class Migrations extends Action $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { + var_dump($error->getPrevious()->getMessage()); + var_dump($error->getPrevious()->getLine()); + var_dump($error->getPrevious()->getFile()); + var_dump($error->getLine()); + var_dump($error->getFile()); /** @var MigrationException $error */ $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } From a008c7c67b315968c74482c2cc2a01d5b576c663 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 19:16:15 +0300 Subject: [PATCH 125/241] Debug info --- src/Appwrite/Platform/Workers/Migrations.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index d4ce30b010..47104c4faf 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -340,11 +340,12 @@ class Migrations extends Action $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; } foreach ($destinationErrors as $error) { - var_dump($error->getPrevious()->getMessage()); - var_dump($error->getPrevious()->getLine()); - var_dump($error->getPrevious()->getFile()); - var_dump($error->getLine()); - var_dump($error->getFile()); + Console::error($error->getPrevious()->getMessage()); + Console::error($error->getPrevious()->getTraceAsString()); + Console::error("Message: " . $error->getMessage()); + Console::error("File: " . $error->getFile()); + Console::error("Line: " . $error->getLine()); + /** @var MigrationException $error */ $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; } @@ -360,8 +361,6 @@ class Migrations extends Action $migration->setAttribute('stage', 'finished'); $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'finished' and succeeded", \microtime(true))); } catch (\Throwable $th) { - - Console::error($th->getMessage()); Console::error($th->getMessage()); Console::error($th->getTraceAsString()); From c5b93ce23163ab0e9fef45030b9d0f5ebc6525e8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 19:33:05 +0300 Subject: [PATCH 126/241] Debug info --- src/Appwrite/Platform/Workers/Migrations.php | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 47104c4faf..c51f4ae8e2 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -332,22 +332,22 @@ class Migrations extends Action $errorMessages = []; foreach ($sourceErrors as $error) { /** @var $sourceErrors $error */ - var_dump($error->getPrevious()->getMessage()); - var_dump($error->getPrevious()->getLine()); - var_dump($error->getPrevious()->getFile()); - var_dump($error->getLine()); - var_dump($error->getFile()); - $errorMessages[] = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; + $message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; + if($error->getPrevious()){ + $message .= " Message: ".$error->getPrevious()->getLine() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); + } + + $errorMessages[] = $message; } foreach ($destinationErrors as $error) { - Console::error($error->getPrevious()->getMessage()); - Console::error($error->getPrevious()->getTraceAsString()); - Console::error("Message: " . $error->getMessage()); - Console::error("File: " . $error->getFile()); - Console::error("Line: " . $error->getLine()); + $message = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; + + if($error->getPrevious()){ + $message .= " Message: ".$error->getPrevious()->getLine() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); + } /** @var MigrationException $error */ - $errorMessages[] = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; + $errorMessages[] = $message; } $migration->setAttribute('errors', $errorMessages); From 6f550688386f7c6a5f50aec7365b810a1d72fd75 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Sep 2024 19:40:04 +0300 Subject: [PATCH 127/241] Fix getMessage --- src/Appwrite/Platform/Workers/Migrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index c51f4ae8e2..c4c2516424 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -334,7 +334,7 @@ class Migrations extends Action /** @var $sourceErrors $error */ $message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; if($error->getPrevious()){ - $message .= " Message: ".$error->getPrevious()->getLine() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); + $message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); } $errorMessages[] = $message; @@ -343,7 +343,7 @@ class Migrations extends Action $message = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; if($error->getPrevious()){ - $message .= " Message: ".$error->getPrevious()->getLine() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); + $message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); } /** @var MigrationException $error */ From c4b742ac0d6ca14bb86597a987826fa55c500882 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Sep 2024 20:52:37 +1200 Subject: [PATCH 128/241] Allow overriding realtime instance --- app/realtime.php | 15 +++++++++++---- src/Appwrite/Messaging/Adapter/Realtime.php | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index b8fdb2cf21..11bcf923ef 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -40,7 +40,7 @@ require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); // Allows overriding -if (!function_exists("getConsoleDB")) { +if (!function_exists('getConsoleDB')) { function getConsoleDB(): Database { global $register; @@ -66,7 +66,7 @@ if (!function_exists("getConsoleDB")) { } // Allows overriding -if (!function_exists("getProjectDB")) { +if (!function_exists('getProjectDB')) { function getProjectDB(Document $project): Database { global $register; @@ -113,7 +113,7 @@ if (!function_exists("getProjectDB")) { } // Allows overriding -if (!function_exists("getCache")) { +if (!function_exists('getCache')) { function getCache(): Cache { global $register; @@ -135,7 +135,14 @@ if (!function_exists("getCache")) { } } -$realtime = new Realtime(); +if (!function_exists('getRealtime')) { + function getRealtime(): Realtime + { + return new Realtime(); + } +} + +$realtime = getRealtime(); /** * Table for statistics across all workers. diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 55d8db2924..49aa305c3f 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -243,7 +243,11 @@ class Realtime extends Adapter * @param string $event * @param Document $payload * @param Document|null $project + * @param Document|null $database + * @param Document|null $collection + * @param Document|null $bucket * @return array + * @throws \Exception */ public static function fromPayload(string $event, Document $payload, Document $project = null, Document $database = null, Document $collection = null, Document $bucket = null): array { From 1848de14cc947c3bcaf6e44308e972080f28582b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 10 Sep 2024 19:30:08 +1200 Subject: [PATCH 129/241] Revert migrations upgrade --- composer.json | 2 +- composer.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 3ecf645817..8ca1ec75e2 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-preserveDates as 0.6.0", + "utopia-php/migration": "0.6.0", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", diff --git a/composer.lock b/composer.lock index 6fb2fe1749..cd695985cc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6991849c4c8e911d1d7bf602cf43497a", + "content-hash": "b6046c157219fbaaad4e2cb6309ff8be", "packages": [ { "name": "adhocore/jwt", @@ -1211,20 +1211,20 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1271,7 +1271,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -1287,7 +1287,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "thecodingmachine/safe", @@ -2608,16 +2608,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.4", + "version": "0.18.5", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "94ab8758fabcefee5c5fa723616e45719833f922" + "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/94ab8758fabcefee5c5fa723616e45719833f922", - "reference": "94ab8758fabcefee5c5fa723616e45719833f922", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/7d355c5e3ccc8ecebc0266f8ddd30088a43be919", + "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919", "shasum": "" }, "require": { @@ -2657,9 +2657,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.4" + "source": "https://github.com/utopia-php/storage/tree/0.18.5" }, - "time": "2024-04-02T08:24:09+00:00" + "time": "2024-09-04T08:57:27+00:00" }, { "name": "utopia-php/swoole", From 707d9554df7ea746819cf8fe4957e7a1f98d1a7f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 10 Sep 2024 12:39:52 +0200 Subject: [PATCH 130/241] Add $createdAt $updatedAt --- .../Utopia/Response/Model/Attribute.php | 13 ++++++++++++- src/Appwrite/Utopia/Response/Model/Index.php | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/Attribute.php b/src/Appwrite/Utopia/Response/Model/Attribute.php index 9f9ceca317..8c43f8d21c 100644 --- a/src/Appwrite/Utopia/Response/Model/Attribute.php +++ b/src/Appwrite/Utopia/Response/Model/Attribute.php @@ -47,7 +47,18 @@ class Attribute extends Model 'required' => false, 'example' => false, ]) - ; + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Attribute creation date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Attribute update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]); } public array $conditions = []; diff --git a/src/Appwrite/Utopia/Response/Model/Index.php b/src/Appwrite/Utopia/Response/Model/Index.php index 3d3d1a3b52..2d795ad439 100644 --- a/src/Appwrite/Utopia/Response/Model/Index.php +++ b/src/Appwrite/Utopia/Response/Model/Index.php @@ -49,13 +49,22 @@ class Index extends Model 'array' => true, 'required' => false, ]) - ; + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Index creation date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Index update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]); } /** * Get Name - * - * @return string */ public function getName(): string { @@ -64,8 +73,6 @@ class Index extends Model /** * Get Collection - * - * @return string */ public function getType(): string { From 82df6bf936e2cd819e5ce4db9d6b52343fdbfbd8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 15 Sep 2024 11:50:49 +0300 Subject: [PATCH 131/241] Add preserve dates --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8ca1ec75e2..3ecf645817 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.6.0", + "utopia-php/migration": "dev-preserveDates as 0.6.0", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From 20fa8b30175a01382dc6b86cc745566f34314a58 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:29:44 +0100 Subject: [PATCH 132/241] fix: flaky functions shot 1 --- .../e2e/Services/Functions/FunctionsBase.php | 25 +++-- .../Functions/FunctionsCustomClientTest.php | 89 ++++++++--------- .../Functions/FunctionsCustomServerTest.php | 98 +++++++++---------- tests/extensions/Async.php | 17 ++++ tests/extensions/Async/Eventually.php | 54 ++++++++++ 5 files changed, 172 insertions(+), 111 deletions(-) create mode 100644 tests/extensions/Async.php create mode 100644 tests/extensions/Async/Eventually.php diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 2d94b9f0e3..26f2ae7aed 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -2,11 +2,14 @@ namespace Tests\E2E\Services\Functions; +use Appwrite\Tests\Async; use Tests\E2E\Client; use Utopia\CLI\Console; trait FunctionsBase { + use Async; + protected string $stdout = ''; protected string $stderr = ''; @@ -15,29 +18,23 @@ trait FunctionsBase Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); } - protected function awaitDeploymentIsBuilt($functionId, $deploymentId, $checkForSuccess = true): void + protected function assertDeployment($functionId, $deploymentId, $allowFailure = false) { - while (true) { + $this->assertEventually(function () use ($functionId, $deploymentId, $allowFailure) { $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]); - if ( - $deployment['headers']['status-code'] >= 400 - || \in_array($deployment['body']['status'], ['ready', 'failed']) - ) { - break; + $status = $deployment['body']['status']; + + if ($allowFailure && $status === 'failed') { + $this->fail('Deployment failed: ' . json_encode($deployment['body']['buildStderr'])); } - \sleep(1); - } - - if ($checkForSuccess) { - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body'])); - } + $this->assertEquals('ready', $status); + }, 180000, 3000); } // /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 92b7c33034..f5f15a0fc6 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\Functions; -use Appwrite\Tests\Retry; use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; @@ -44,7 +43,6 @@ class FunctionsCustomClientTest extends Scope return []; } - #[Retry(count: 2)] public function testCreateExecution(): array { /** @@ -120,7 +118,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -148,25 +146,26 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(202, $execution['headers']['status-code']); - // Wait for the first scheduled execution to be created - sleep(90); + $this->assertEventually(function () use ($function) { + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(2, $executions['body']['executions']); - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(2, $executions['body']['executions']); - $this->assertIsArray($executions['body']['executions']); - $this->assertEquals($executions['body']['executions'][1]['trigger'], 'schedule'); - $this->assertEquals($executions['body']['executions'][1]['status'], 'completed'); - $this->assertEquals($executions['body']['executions'][1]['responseStatusCode'], 200); - $this->assertEquals($executions['body']['executions'][1]['responseBody'], ''); - $this->assertNotEmpty($executions['body']['executions'][1]['logs'], ''); - $this->assertNotEmpty($executions['body']['executions'][1]['errors'], ''); - $this->assertGreaterThan(0, $executions['body']['executions'][1]['duration']); + // Check if the scheduled execution has completed + $scheduledExecution = $executions['body']['executions'][1]; + $this->assertEquals('schedule', $scheduledExecution['trigger']); + $this->assertEquals('completed', $scheduledExecution['status']); + $this->assertEquals(200, $scheduledExecution['responseStatusCode']); + $this->assertEquals('', $scheduledExecution['responseBody']); + $this->assertNotEmpty($scheduledExecution['logs']); + $this->assertNotEmpty($scheduledExecution['errors']); + $this->assertGreaterThan(0, $scheduledExecution['duration']); + }, 120000, 2000); // Cleanup : Delete function $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $function['body']['$id'], [ @@ -216,7 +215,7 @@ class FunctionsCustomClientTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, true); + $this->assertDeployment($function['body']['$id'], $deploymentId, true); // Schedule execution for the future \date_default_timezone_set('UTC'); @@ -245,25 +244,22 @@ class FunctionsCustomClientTest extends Scope $executionId = $execution['body']['$id']; - $start = \microtime(true); - while (true) { + $this->assertEventually(function () use ($function, $executionId) { $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]); - if ($execution['body']['status'] === 'completed') { - break; - } + $this->assertEquals('completed', $execution['body']['status']); + }, 150000, 2000); // Timeout after 150 seconds, wait 2 seconds between retries - $timeout = 60 + 60 + 15; // up to 1 minute round up, 1 minute schedule postpone, 15s cold start safety - if (\microtime(true) - $start > $timeout) { - $this->fail('Scheduled execution did not complete with status ' . $execution['body']['status'] . ': ' . \json_encode($execution)); - } - - usleep(500000); // 0.5 seconds - } + // After ensuring the execution is completed, fetch it again for assertions + $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); $this->assertEquals(200, $execution['headers']['status-code']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -409,7 +405,7 @@ class FunctionsCustomClientTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -458,17 +454,16 @@ class FunctionsCustomClientTest extends Scope $executionId = $execution['body']['$id'] ?? ''; - sleep(5); + $this->assertEventually(function () use ($functionId, $executionId, $projectId, $apikey) { + $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $apikey, + ]); - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ]); - - $this->assertEmpty($execution['body']['responseBody']); - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + }, 30000, 1000); return [ 'functionId' => $functionId @@ -521,7 +516,7 @@ class FunctionsCustomClientTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); // Why do we have to do this? $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ @@ -760,7 +755,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -853,7 +848,7 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2958e6cb5f..4005279560 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -3,7 +3,6 @@ namespace Tests\E2E\Services\Functions; use Appwrite\Functions\Specification; -use Appwrite\Tests\Retry; use CURLFile; use PHPUnit\Framework\ExpectationFailedException; use Tests\E2E\Client; @@ -429,7 +428,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $functionDetails = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -521,7 +520,7 @@ class FunctionsCustomServerTest extends Scope // Wait for deployment build to finish // Deployment is automatically activated - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId); + $this->assertDeployment($function['body']['$id'], $deploymentId); $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -635,7 +634,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); - $this->awaitDeploymentIsBuilt($data['functionId'], $deploymentId); + $this->assertDeployment($data['functionId'], $deploymentId); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', @@ -650,7 +649,7 @@ class FunctionsCustomServerTest extends Scope $deploymentIdInactive = $deployment['body']['$id']; - $this->awaitDeploymentIsBuilt($data['functionId'], $deploymentIdInactive); + $this->assertDeployment($data['functionId'], $deploymentIdInactive); $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', @@ -696,26 +695,16 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); - // Poll until deployment is in progress - while (true) { + $this->assertEventually(function () use ($data, $deploymentId) { $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]); - if ( - $deployment['headers']['status-code'] >= 400 - || $deployment['body']['status'] === 'building' - ) { - break; - } - - \sleep(1); - } - - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('building', $deployment['body']['status']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('building', $deployment['body']['status']); + }, 60000, 1000); // Cancel the deployment build $cancel = $this->client->call(Client::METHOD_PATCH, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId . '/build', [ @@ -729,10 +718,10 @@ class FunctionsCustomServerTest extends Scope /** * Build worker still runs the build. - * 30s sleep gives worker enough time to finish build. + * 10s sleep gives worker enough time to finish build. * After build finished, it should still be canceled, not ready. */ - \sleep(30); + \sleep(10); $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -794,7 +783,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $largeTag['body']['$id']; - $this->awaitDeploymentIsBuilt($data['functionId'], $deploymentId, true); + $this->assertDeployment($data['functionId'], $deploymentId, true); $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -1294,7 +1283,6 @@ class FunctionsCustomServerTest extends Scope /** * @depends testUpdateDeployment */ - #[Retry(count: 2)] public function testSyncCreateExecution($data): array { /** @@ -1417,14 +1405,23 @@ class FunctionsCustomServerTest extends Scope $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(202, $execution['headers']['status-code']); - sleep(5); - $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, array_merge([ + + $this->assertEventually(function () use ($data, $executionId) { + $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals('waiting', $execution['body']['status']); + }, 10000, 500); + + $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + ]); $this->assertEquals(204, $execution['headers']['status-code']); - $this->assertEmpty($execution['body']); return $data; } @@ -1432,7 +1429,6 @@ class FunctionsCustomServerTest extends Scope /** * @depends testGetExecution */ - #[Retry(count: 2)] public function testUpdateSpecs($data): array { /** @@ -1622,7 +1618,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($functionId, $deploymentId); + $this->assertDeployment($functionId, $deploymentId); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', @@ -1773,7 +1769,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -1868,7 +1864,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -1954,7 +1950,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2054,7 +2050,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2147,7 +2143,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2169,21 +2165,23 @@ class FunctionsCustomServerTest extends Scope ]); $userId = $user['body']['$id']; - $this->assertEquals(201, $user['headers']['status-code']); - // Wait for execution to occur - sleep(15); + $execution = null; + $this->assertEventually(function () use ($functionId, &$execution) { + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + if (empty($executions['body']['executions'])) { + throw new \Exception('No executions found yet'); + } - $execution = $executions['body']['executions'][0]; + $execution = $executions['body']['executions'][0]; + $this->assertEquals('completed', $execution['status']); + }, 30000, 1000); - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertEquals('completed', $execution['status']); $this->assertEquals(204, $execution['responseStatusCode']); $this->assertStringContainsString($userId, $execution['logs']); $this->assertStringContainsString('Event User', $execution['logs']); @@ -2243,7 +2241,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2346,7 +2344,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2437,7 +2435,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2553,7 +2551,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2639,7 +2637,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false); + $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2735,7 +2733,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; - $this->awaitDeploymentIsBuilt($functionId, $deploymentId, checkForSuccess: false); + $this->assertDeployment($functionId, $deploymentId, allowFailure: true); // Sync Executions test $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ diff --git a/tests/extensions/Async.php b/tests/extensions/Async.php new file mode 100644 index 0000000000..48af24bc02 --- /dev/null +++ b/tests/extensions/Async.php @@ -0,0 +1,17 @@ +timeoutMs = $timeoutMs; + $this->waitMs = $waitMs; + } + + public function evaluate(mixed $probe, string $description = '', bool $returnResult = false): ?bool + { + if (!is_callable($probe)) { + throw new \Exception('Probe must be a callable'); + } + + $start = microtime(true); + $lastException = null; + + do { + try { + $probe(); + return true; + } catch (\Exception $exception) { + $lastException = $exception; + } + + usleep($this->waitMs * 1000); + } while (microtime(true) - $start < $this->timeoutMs / 1000); + + if ($returnResult) { + return false; + } + + throw $lastException; + } + + protected function failureDescription(mixed $other): string + { + return 'the given probe was satisfied within ' . $this->timeoutMs . 'ms.'; + } + + public function toString(): string + { + return 'Eventually'; + } +} From 121b9f32b816b26d3a4afb49a1e0a57e8f8d6be2 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:50:36 +0100 Subject: [PATCH 133/241] fix: allowFailure --- .../e2e/Services/Functions/FunctionsBase.php | 2 +- .../Functions/FunctionsCustomServerTest.php | 177 ++++-------------- 2 files changed, 40 insertions(+), 139 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 26f2ae7aed..7f33e0b2ee 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -34,7 +34,7 @@ trait FunctionsBase } $this->assertEquals('ready', $status); - }, 180000, 3000); + }, 180000, 1000); } // /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 4005279560..193e99d89e 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1631,33 +1631,32 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $execution['headers']['status-code']); - sleep(20); + $executionData = null; - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('trigger', ['http'])->toString(), - ], - ]); + $this->assertEventually(function () use ($functionId, $executionId, &$executionData) { + $executionData = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); - $this->assertEquals($executions['headers']['status-code'], 200); - $this->assertEquals($executions['body']['total'], 1); - $this->assertIsArray($executions['body']['executions']); - $this->assertCount(1, $executions['body']['executions']); - $this->assertEquals($executions['body']['executions'][0]['$id'], $executionId); - $this->assertEquals($executions['body']['executions'][0]['trigger'], 'http'); - $this->assertEquals($executions['body']['executions'][0]['status'], 'failed'); - $this->assertEquals($executions['body']['executions'][0]['responseStatusCode'], 500); - $this->assertGreaterThan(2, $executions['body']['executions'][0]['duration']); - $this->assertLessThan(20, $executions['body']['executions'][0]['duration']); - $this->assertEquals($executions['body']['executions'][0]['responseBody'], ''); - $this->assertEquals($executions['body']['executions'][0]['logs'], ''); - $this->assertStringContainsString('timed out', $executions['body']['executions'][0]['errors']); + $this->assertEquals(200, $executionData['headers']['status-code']); + $this->assertEquals('failed', $executionData['body']['status']); - $start = \microtime(true); - while (true) { + if ($executionData['body']['status'] !== 'failed') { + } + }, 30000, 1000); + + $this->assertEquals('failed', $executionData['body']['status']); + $this->assertEquals(500, $executionData['body']['responseStatusCode']); + $this->assertGreaterThan(2, $executionData['body']['duration']); + $this->assertLessThan(20, $executionData['body']['duration']); + $this->assertEquals('', $executionData['body']['responseBody']); + $this->assertEquals('', $executionData['body']['logs']); + $this->assertStringContainsString('timed out', $executionData['body']['errors']); + + $scheduledExecutionData = null; + + $this->assertEventually(function () use ($functionId, &$scheduledExecutionData) { $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1668,27 +1667,12 @@ class FunctionsCustomServerTest extends Scope ]); $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals(1, $executions['body']['total']); - if (\count($executions['body']['executions']) > 0) { - break; - } + $scheduledExecutionData = $executions['body']['executions'][0]; + }, 140000, 1000); // Wait up to 140 seconds, checking every 1 second - // 0s would mean instant execution - // +60 seconds, maximum possible waiting time before next minute - // +10 seconds, maximum update interval time - // +60 seconds, possible overlap between update and schedule tick - // +10 seconds, maximum execution time including cold-start - // Result: We allow maximum - if (\microtime(true) - $start > 140) { - $this->fail('Execution did not create within 140 seconds of schedule: ' . \json_encode($executions)); - } - - usleep(1000000); // 1 second - } - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertGreaterThanOrEqual(1, \count($executions['body']['executions'])); - $this->assertEquals($executions['body']['executions'][0]['trigger'], 'schedule'); + $this->assertEquals('schedule', $scheduledExecutionData['trigger']); // Cleanup : Delete function $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ @@ -1700,6 +1684,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + /** * * @return array @@ -1769,14 +1754,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertDeployment($function['body']['$id'], $deploymentId); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', @@ -1864,17 +1842,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'x-appwrite-project' => $this->getProject()['$id'], @@ -1950,17 +1918,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $bytes = pack('C*', ...[0, 20, 255]); @@ -2050,14 +2008,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertDeployment($function['body']['$id'], $deploymentId); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', @@ -2143,17 +2094,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); // Create user to trigger event $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ @@ -2241,7 +2182,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); + $this->assertDeployment($function['body']['$id'], $deploymentId); $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -2344,17 +2285,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; @@ -2435,17 +2366,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; @@ -2551,17 +2472,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -2637,17 +2548,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeployment($function['body']['$id'], $deploymentId, allowFailure: true); - - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); + $this->assertDeployment($function['body']['$id'], $deploymentId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -2733,7 +2634,7 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertDeployment($functionId, $deploymentId, allowFailure: true); + $this->assertDeployment($functionId, $deploymentId); // Sync Executions test $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ From 979fcacd6cab9bb48aae251f665ad5b576c803ba Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:35:52 +0100 Subject: [PATCH 134/241] fix: abstract requests --- .../e2e/Services/Functions/FunctionsBase.php | 391 +++++---- .../Functions/FunctionsConsoleClientTest.php | 173 ++-- .../Functions/FunctionsCustomClientTest.php | 739 +++--------------- .../Functions/FunctionsCustomServerTest.php | 358 +++------ 4 files changed, 469 insertions(+), 1192 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 7f33e0b2ee..92210a3027 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Functions; use Appwrite\Tests\Async; +use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; @@ -13,220 +14,212 @@ trait FunctionsBase protected string $stdout = ''; protected string $stderr = ''; - protected function packageCode($folder) + protected function setupFunction(mixed $params): string { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $params); + $functionId = $function['body']['$id']; + + $this->assertEquals($function['headers']['status-code'], 201); + return $functionId; + } + + protected function setupDeployment(string $functionId, mixed $params): string + { + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $params); + $deploymentId = $deployment['body']['$id']; + + $this->assertEquals($deployment['headers']['status-code'], 202); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('completed', $deployment['body']['status']); + }, 200000, 500); + + return $deploymentId; + } + + protected function cleanupFunction(string $functionId): void + { + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals($function['headers']['status-code'], 204); + } + + protected function createFunction(mixed $params) + { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + $functionId = $function['body']['$id']; + + if (empty($variables)) { + return $function; + } + + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $function; + } + + protected function createVariable($functionId, $key, $value) + { + $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => $key, + 'value' => $value + ]); + + return $variable; + } + + protected function getFunction($functionId) + { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $function; + } + + protected function getDeployment($functionId, $deploymentId) + { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $deployment; + } + + protected function getExecution($functionId, $executionId) + { + $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $execution; + } + + protected function listDeployments($functionId, $params = []) + { + $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders(), $params)); + + return $deployments; + } + + protected function listExecutions($functionId, $params = []) + { + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders(), $params)); + + return $executions; + } + + protected function packageFunction(string $folder = 'php') + { + $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + + if (!file_exists($code)) { + throw new \Exception('Failed to create code package. ' . $code . ' does not exist.'); + } + if (filesize($code) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($code, 'application/x-gzip', \basename($code)); } - protected function assertDeployment($functionId, $deploymentId, $allowFailure = false) - { - $this->assertEventually(function () use ($functionId, $deploymentId, $allowFailure) { - $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + protected function createDeployment( + string $functionId, + mixed $params, + bool $cli = false, + bool $admin = false + ) { + $authHeaders = $this->getHeaders(); + + if ($admin) { + $authHeaders = [ 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + ]; + } + if ($cli) { + $authHeaders[] = 'x-sdk-language: cli'; + } - $status = $deployment['body']['status']; + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], - if ($allowFailure && $status === 'failed') { - $this->fail('Deployment failed: ' . json_encode($deployment['body']['buildStderr'])); - } + ], $params); - $this->assertEquals('ready', $status); - }, 180000, 1000); + return $deployment; } - // /** - // * @depends testCreateTeam - // */ - // public function testGetTeam($data):array - // { - // $id = $data['teamUid'] ?? ''; + protected function getFunctionUsage($functionId, $params) + { + $usage = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders(), $params)); - // /** - // * Test for SUCCESS - // */ - // $response = $this->client->call(Client::METHOD_GET, '/teams/'.$id, array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders())); + return $usage; + } - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertNotEmpty($response['body']['$id']); - // $this->assertEquals('Arsenal', $response['body']['name']); - // $this->assertGreaterThan(-1, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertIsInt($response['body']['dateCreated']); + protected function getTemplate(string $templateId) + { + $template = $this->client->call(Client::METHOD_GET, '/functions/templates/' . $templateId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); - // /** - // * Test for FAILURE - // */ + return $template; + } - // return []; - // } + protected function createExecution(string $functionId, mixed $params) + { + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); - // /** - // * @depends testCreateTeam - // */ - // public function testListTeams($data):array - // { - // /** - // * Test for SUCCESS - // */ - // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders())); + return $execution; + } - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertGreaterThan(0, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertCount(3, $response['body']['teams']); + protected function deleteFunction($functionId) + { + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); - // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'limit' => 2, - // ]); - - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertGreaterThan(0, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertCount(2, $response['body']['teams']); - - // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'offset' => 1, - // ]); - - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertGreaterThan(0, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertCount(2, $response['body']['teams']); - - // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'search' => 'Manchester', - // ]); - - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertGreaterThan(0, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertCount(1, $response['body']['teams']); - // $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']); - - // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'search' => 'United', - // ]); - - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertGreaterThan(0, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertCount(1, $response['body']['teams']); - // $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']); - - // /** - // * Test for FAILURE - // */ - - // return []; - // } - - // public function testUpdateTeam():array - // { - // /** - // * Test for SUCCESS - // */ - // $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'name' => 'Demo' - // ]); - - // $this->assertEquals(201, $response['headers']['status-code']); - // $this->assertNotEmpty($response['body']['$id']); - // $this->assertEquals('Demo', $response['body']['name']); - // $this->assertGreaterThan(-1, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertIsInt($response['body']['dateCreated']); - - // $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'name' => 'Demo New' - // ]); - - // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertNotEmpty($response['body']['$id']); - // $this->assertEquals('Demo New', $response['body']['name']); - // $this->assertGreaterThan(-1, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertIsInt($response['body']['dateCreated']); - - // /** - // * Test for FAILURE - // */ - // $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // ]); - - // $this->assertEquals(400, $response['headers']['status-code']); - - // return []; - // } - - // public function testDeleteTeam():array - // { - // /** - // * Test for SUCCESS - // */ - // $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), [ - // 'name' => 'Demo' - // ]); - - // $teamUid = $response['body']['$id']; - - // $this->assertEquals(201, $response['headers']['status-code']); - // $this->assertNotEmpty($response['body']['$id']); - // $this->assertEquals('Demo', $response['body']['name']); - // $this->assertGreaterThan(-1, $response['body']['total']); - // $this->assertIsInt($response['body']['total']); - // $this->assertIsInt($response['body']['dateCreated']); - - // $response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid, array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders())); - - // $this->assertEquals(204, $response['headers']['status-code']); - // $this->assertEmpty($response['body']); - - // /** - // * Test for FAILURE - // */ - // $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid, array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders())); - - // $this->assertEquals(404, $response['headers']['status-code']); - - // return []; - // } + return $function; + } } diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 8cb7f6f869..6ee5a1085d 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -13,13 +13,11 @@ class FunctionsConsoleClientTest extends Scope { use ProjectCustom; use SideConsole; + use FunctionsBase; public function testCreateFunction(): array { - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function = $this->createFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::user($this->getUser()['$id'])->toString()], @@ -32,87 +30,69 @@ class FunctionsConsoleClientTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); - + $functionId = $function['body']['$id']; $this->assertEquals(201, $function['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function2 = $this->createFunction([ 'functionId' => ID::unique(), 'name' => 'Test Failure', 'execute' => ['some-random-string'], 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', ]); - - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(400, $function2['headers']['status-code']); return [ - 'functionId' => $function['body']['$id'] + 'functionId' => $functionId, ]; } /** * @depends testCreateFunction */ - public function testGetCollectionUsage(array $data) + public function testFunctionUsage(array $data) { - /** - * Test for FAILURE - */ - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ - 'range' => '232h' - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/randomFunctionId/usage', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ - 'range' => '24h' - ]); - - $this->assertEquals(404, $response['headers']['status-code']); - /** * Test for SUCCESS */ - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ + $usage = $this->getFunctionUsage($data['functionId'], [ 'range' => '24h' ]); + $this->assertEquals(200, $usage['headers']['status-code']); + $this->assertEquals(19, count($usage['body'])); + $this->assertEquals('24h', $usage['body']['range']); + $this->assertIsNumeric($usage['body']['deploymentsTotal']); + $this->assertIsNumeric($usage['body']['deploymentsStorageTotal']); + $this->assertIsNumeric($usage['body']['buildsTotal']); + $this->assertIsNumeric($usage['body']['buildsStorageTotal']); + $this->assertIsNumeric($usage['body']['buildsTimeTotal']); + $this->assertIsNumeric($usage['body']['buildsMbSecondsTotal']); + $this->assertIsNumeric($usage['body']['executionsTotal']); + $this->assertIsNumeric($usage['body']['executionsTimeTotal']); + $this->assertIsNumeric($usage['body']['executionsMbSecondsTotal']); + $this->assertIsArray($usage['body']['deployments']); + $this->assertIsArray($usage['body']['deploymentsStorage']); + $this->assertIsArray($usage['body']['builds']); + $this->assertIsArray($usage['body']['buildsTime']); + $this->assertIsArray($usage['body']['buildsStorage']); + $this->assertIsArray($usage['body']['buildsTime']); + $this->assertIsArray($usage['body']['buildsMbSeconds']); + $this->assertIsArray($usage['body']['executions']); + $this->assertIsArray($usage['body']['executionsTime']); + $this->assertIsArray($usage['body']['executionsMbSeconds']); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(19, count($response['body'])); - $this->assertEquals('24h', $response['body']['range']); - $this->assertIsNumeric($response['body']['deploymentsTotal']); - $this->assertIsNumeric($response['body']['deploymentsStorageTotal']); - $this->assertIsNumeric($response['body']['buildsTotal']); - $this->assertIsNumeric($response['body']['buildsStorageTotal']); - $this->assertIsNumeric($response['body']['buildsTimeTotal']); - $this->assertIsNumeric($response['body']['buildsMbSecondsTotal']); - $this->assertIsNumeric($response['body']['executionsTotal']); - $this->assertIsNumeric($response['body']['executionsTimeTotal']); - $this->assertIsNumeric($response['body']['executionsMbSecondsTotal']); - $this->assertIsArray($response['body']['deployments']); - $this->assertIsArray($response['body']['deploymentsStorage']); - $this->assertIsArray($response['body']['builds']); - $this->assertIsArray($response['body']['buildsTime']); - $this->assertIsArray($response['body']['buildsStorage']); - $this->assertIsArray($response['body']['buildsTime']); - $this->assertIsArray($response['body']['buildsMbSeconds']); - $this->assertIsArray($response['body']['executions']); - $this->assertIsArray($response['body']['executionsTime']); - $this->assertIsArray($response['body']['executionsMbSeconds']); + /** + * Test for FAILURE + */ + $usage = $this->getFunctionUsage($data['functionId'], [ + 'range' => '232h' + ]); + $this->assertEquals(400, $usage['headers']['status-code']); + + $usage = $this->getFunctionUsage('randomFunctionId', [ + 'range' => '24h' + ]); + $this->assertEquals(404, $usage['headers']['status-code']); } /** @@ -123,31 +103,40 @@ class FunctionsConsoleClientTest extends Scope /** * Test for SUCCESS */ - - $response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'APP_TEST', - 'value' => 'TESTINGVALUE' - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $variableId = $response['body']['$id']; + $variable = $this->createVariable( + $data['functionId'], + 'APP_TEST', + 'TESTINGVALUE' + ); + $variableId = $variable['body']['$id']; + $this->assertEquals(201, $variable['headers']['status-code']); /** * Test for FAILURE */ + // Test for duplicate key + $variable = $this->createVariable( + $data['functionId'], + 'APP_TEST', + 'ANOTHER_TESTINGVALUE' + ); + $this->assertEquals(409, $variable['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'APP_TEST', - 'value' => 'ANOTHER_TESTINGVALUE' - ]); + // Test for invalid key + $variable = $this->createVariable( + $data['functionId'], + str_repeat("A", 256), + 'TESTINGVALUE' + ); + $this->assertEquals(400, $variable['headers']['status-code']); - $this->assertEquals(409, $response['headers']['status-code']); + // Test for invalid value + $variable = $this->createVariable( + $data['functionId'], + 'LONGKEY', + str_repeat("#", 8193) + ); + $this->assertEquals(400, $variable['headers']['status-code']); return array_merge( $data, @@ -155,28 +144,6 @@ class FunctionsConsoleClientTest extends Scope 'variableId' => $variableId ] ); - - $longKey = str_repeat("A", 256); - $response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => $longKey, - 'value' => 'TESTINGVALUE' - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - $longValue = str_repeat("#", 8193); - $response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'LONGKEY', - 'value' => $longValue - ]); - - $this->assertEquals(400, $response['headers']['status-code']); } /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index f5f15a0fc6..872249085e 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -2,16 +2,12 @@ namespace Tests\E2E\Services\Functions; -use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; -use Utopia\Config\Config; -use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; -use Utopia\Database\Query; class FunctionsCustomClientTest extends Scope { @@ -19,15 +15,12 @@ class FunctionsCustomClientTest extends Scope use ProjectCustom; use SideClient; - public function testCreate(): array + public function testCreate() { /** - * Test for SUCCESS + * Test for FAILURE */ - $response1 = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function = $this->createFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'events' => [ @@ -37,22 +30,15 @@ class FunctionsCustomClientTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); - - $this->assertEquals(401, $response1['headers']['status-code']); - - return []; + $this->assertEquals(401, $function['headers']['status-code']); } - public function testCreateExecution(): array + public function testCreateExecution() { /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::user($this->getUser()['$id'])->toString()], @@ -62,121 +48,41 @@ class FunctionsCustomClientTest extends Scope 'users.*.create', 'users.*.delete', ], - 'schedule' => '* * * * *', // execute every minute + 'schedule' => '* * * * *', // Execute every minute 'timeout' => 10, ]); - - $this->assertEquals(201, $function['headers']['status-code']); - - /** Create Variables */ - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'key' => 'funcKey1', - 'value' => 'funcValue1', - ]); - - $variable2 = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'key' => 'funcKey2', - 'value' => 'funcValue2', - ]); - - $variable3 = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'key' => 'funcKey3', - 'value' => 'funcValue3', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - $this->assertEquals(201, $variable2['headers']['status-code']); - $this->assertEquals(201, $variable3['headers']['status-code']); - - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(200, $function['headers']['status-code']); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], [ - 'async' => true, + $execution = $this->createExecution($functionId, [ + 'async' => false, ]); - $this->assertEquals(401, $execution['headers']['status-code']); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true, ]); - $this->assertEquals(202, $execution['headers']['status-code']); - $this->assertEventually(function () use ($function) { - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - + $this->assertEventually(function () use ($functionId) { + $executions = $this->listExecutions($functionId); $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(2, $executions['body']['executions']); - // Check if the scheduled execution has completed - $scheduledExecution = $executions['body']['executions'][1]; - $this->assertEquals('schedule', $scheduledExecution['trigger']); - $this->assertEquals('completed', $scheduledExecution['status']); - $this->assertEquals(200, $scheduledExecution['responseStatusCode']); - $this->assertEquals('', $scheduledExecution['responseBody']); - $this->assertNotEmpty($scheduledExecution['logs']); - $this->assertNotEmpty($scheduledExecution['errors']); - $this->assertGreaterThan(0, $scheduledExecution['duration']); - }, 120000, 2000); + $asyncExecution = $executions['body']['executions'][1]; + $this->assertEquals('schedule', $asyncExecution['trigger']); + $this->assertEquals('completed', $asyncExecution['status']); + $this->assertEquals(200, $asyncExecution['responseStatusCode']); + $this->assertEquals('', $asyncExecution['responseBody']); + $this->assertNotEmpty($asyncExecution['logs']); + $this->assertNotEmpty($asyncExecution['errors']); + $this->assertGreaterThan(0, $asyncExecution['duration']); + }, 100000, 250); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $function['body']['$id'], [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); - - return []; + $this->cleanupFunction($functionId); } public function testCreateScheduledExecution(): void @@ -184,11 +90,7 @@ class FunctionsCustomClientTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::user($this->getUser()['$id'])->toString()], @@ -196,38 +98,20 @@ class FunctionsCustomClientTest extends Scope 'entrypoint' => 'index.php', 'timeout' => 10, ]); - - $this->assertEquals(201, $function['headers']['status-code']); - - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId, true); // Schedule execution for the future \date_default_timezone_set('UTC'); - $futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); + $futureTime = (new \DateTime())->add(new \DateInterval('PT30S')); // 30 seconds from now $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + 'scheduledAt' => $futureTime->format(\DateTime::ATOM), 'path' => '/custom-path', 'method' => 'PATCH', 'body' => 'custom-body', @@ -235,101 +119,60 @@ class FunctionsCustomClientTest extends Scope 'x-custom-header' => 'custom-value' ] ]); - + $executionId = $execution['body']['$id']; $this->assertEquals(202, $execution['headers']['status-code']); $this->assertEquals('scheduled', $execution['body']['status']); $this->assertEquals('PATCH', $execution['body']['requestMethod']); $this->assertEquals('/custom-path', $execution['body']['requestPath']); $this->assertCount(0, $execution['body']['requestHeaders']); - $executionId = $execution['body']['$id']; - - $this->assertEventually(function () use ($function, $executionId) { - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); $this->assertEquals('completed', $execution['body']['status']); - }, 150000, 2000); // Timeout after 150 seconds, wait 2 seconds between retries - - // After ensuring the execution is completed, fetch it again for assertions - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals(200, $execution['body']['responseStatusCode']); - $this->assertEquals('completed', $execution['body']['status']); - $this->assertEquals('/custom-path', $execution['body']['requestPath']); - $this->assertEquals('PATCH', $execution['body']['requestMethod']); - $this->assertStringContainsString('body-is-custom-body', $execution['body']['logs']); - $this->assertStringContainsString('custom-header-is-custom-value', $execution['body']['logs']); - $this->assertStringContainsString('method-is-patch', $execution['body']['logs']); - $this->assertStringContainsString('path-is-/custom-path', $execution['body']['logs']); - $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); - $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); - $this->assertGreaterThan(0, $execution['body']['duration']); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals('/custom-path', $execution['body']['requestPath']); + $this->assertEquals('PATCH', $execution['body']['requestMethod']); + $this->assertStringContainsString('body-is-custom-body', $execution['body']['logs']); + $this->assertStringContainsString('custom-header-is-custom-value', $execution['body']['logs']); + $this->assertStringContainsString('method-is-patch', $execution['body']['logs']); + $this->assertStringContainsString('path-is-/custom-path', $execution['body']['logs']); + $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); + $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); + $this->assertGreaterThan(0, $execution['body']['duration']); + }, 40000, 2500); /* Test for FAILURE */ - // Schedule synchronous execution - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => false, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + 'scheduledAt' => $futureTime->format(\DateTime::ATOM), ]); - $this->assertEquals(400, $execution['headers']['status-code']); // Execution with seconds precision - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true, 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02"))->format(\DateTime::ATOM) ]); - $this->assertEquals(400, $execution['headers']['status-code']); // Execution with milliseconds precision - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true, 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02.255"))->format(\DateTime::ATOM) ]); - $this->assertEquals(400, $execution['headers']['status-code']); // Execution too soon - $futureTime = (new \DateTime())->add(new \DateInterval('PT1M')); - $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + 'scheduledAt' => (new \DateTime())->add(new \DateInterval('PT1S'))->format(\DateTime::ATOM) ]); - $this->assertEquals(400, $execution['headers']['status-code']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $function['body']['$id'], [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId, $executionId); } public function testCreateCustomExecution(): array @@ -337,14 +180,7 @@ class FunctionsCustomClientTest extends Scope /** * Test for SUCCESS */ - $projectId = $this->getProject()['$id']; - $apikey = $this->getProject()['apiKey']; - - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::any()->toString()], @@ -352,77 +188,16 @@ class FunctionsCustomClientTest extends Scope 'entrypoint' => 'index.php', 'timeout' => 10, ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - /** Create Variables */ - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey1', - 'value' => 'funcValue1', - ]); - - $variable2 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey2', - 'value' => 'funcValue2', - ]); - - $variable3 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey3', - 'value' => 'funcValue3', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - $this->assertEquals(201, $variable2['headers']['status-code']); - $this->assertEquals(201, $variable3['headers']['status-code']); - - $folder = 'php-fn'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $deploymentId = $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), //different tarball names intentional + 'code' => $this->packageFunction('php-fn'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], []); - - $this->assertEquals(200, $function['headers']['status-code']); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', 'async' => false ]); - $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -440,30 +215,20 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); $this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']); $this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']); - $this->assertEquals($projectId, $output['APPWRITE_FUNCTION_PROJECT_ID']); + $this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', 'async' => true ]); - + $executionId = $execution['body']['$id']; $this->assertEquals(202, $execution['headers']['status-code']); - $executionId = $execution['body']['$id'] ?? ''; - - $this->assertEventually(function () use ($functionId, $executionId, $projectId, $apikey) { - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ]); - + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); - }, 30000, 1000); + }, 10000, 250); return [ 'functionId' => $functionId @@ -475,14 +240,7 @@ class FunctionsCustomClientTest extends Scope /** * Test for SUCCESS */ - $projectId = $this->getProject()['$id']; - $apikey = $this->getProject()['apiKey']; - - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::any()->toString()], @@ -495,56 +253,25 @@ class FunctionsCustomClientTest extends Scope ], 'timeout' => 10, ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $folder = 'php-fn'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), //different tarball names intentional + 'code' => $this->packageFunction('php-fn'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - // Why do we have to do this? - $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], []); - - $this->assertEquals(200, $function['headers']['status-code']); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, + 'x-appwrite-project' => $this->getProject()['$id'], ], [ 'data' => 'foobar', 'async' => true, ]); - $this->assertEquals(202, $execution['headers']['status-code']); } - public function testCreateExecutionNoDeployment(): array + public function testCreateExecutionNoDeployment() { - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [], @@ -553,146 +280,18 @@ class FunctionsCustomClientTest extends Scope 'timeout' => 10, ]); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], [ - 'async' => true, + $execution = $this->createExecution($functionId, [ + 'async' => true ]); - $this->assertEquals(404, $execution['headers']['status-code']); - - return []; } - /** - * @depends testCreateCustomExecution - */ - public function testListExecutions(array $data) - { - $functionId = $data['functionId']; - $projectId = $this->getProject()['$id']; - $apikey = $this->getProject()['apiKey']; - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], $this->getHeaders()), [ - 'data' => 'foobar' - ]); - - $this->assertEquals(201, $execution['headers']['status-code']); - - $base = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ]); - - $this->assertEquals(200, $base['headers']['status-code']); - $this->assertCount(3, $base['body']['executions']); - $this->assertEquals('completed', $base['body']['executions'][0]['status']); - $this->assertEquals('completed', $base['body']['executions'][1]['status']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::limit(1)->toString(), - ], - ]); - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(1, $executions['body']['executions']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::offset(1)->toString(), - ], - ]); - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(2, $executions['body']['executions']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::equal('status', ['completed'])->toString(), - ], - ]); - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(3, $executions['body']['executions']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::equal('status', ['failed'])->toString(), - ], - ]); - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(0, $executions['body']['executions']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::cursorAfter(new Document(['$id' => $base['body']['executions'][0]['$id']]))->toString(), - ], - ]); - - $this->assertCount(2, $executions['body']['executions']); - $this->assertEquals($base['body']['executions'][1]['$id'], $executions['body']['executions'][0]['$id']); - - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'queries' => [ - Query::cursorBefore(new Document(['$id' => $base['body']['executions'][1]['$id']]))->toString(), - ], - ]); - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); - } - - public function testSynchronousExecution(): array + public function testSynchronousExecution() { /** * Test for SUCCESS */ - - $projectId = $this->getProject()['$id']; - $apikey = $this->getProject()['apiKey']; - - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::any()->toString()], @@ -700,79 +299,16 @@ class FunctionsCustomClientTest extends Scope 'entrypoint' => 'index.php', 'timeout' => 10, ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - /** Create Variables */ - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey1', - 'value' => 'funcValue1', - ]); - - $variable2 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey2', - 'value' => 'funcValue2', - ]); - - $variable3 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ - 'key' => 'funcKey3', - 'value' => 'funcValue3', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - $this->assertEquals(201, $variable2['headers']['status-code']); - $this->assertEquals(201, $variable3['headers']['status-code']); - - $folder = 'php-fn'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $deploymentId = $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), //different tarball names intentional + 'code' => $this->packageFunction('php-fn'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ]); - - $this->assertEquals(200, $function['headers']['status-code']); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', // Testing default value, should be 'async' => false ]); - $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); @@ -789,67 +325,29 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']); $this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']); $this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']); - $this->assertEquals($projectId, $output['APPWRITE_FUNCTION_PROJECT_ID']); + $this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']); // Client should never see logs and errors $this->assertEmpty($execution['body']['logs']); $this->assertEmpty($execution['body']['errors']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); - - return []; + $this->cleanupFunction($functionId); } - public function testNonOverrideOfHeaders(): array + public function testNonOverrideOfHeaders() { - /** - * Test for SUCCESS - */ - $projectId = $this->getProject()['$id']; - $apikey = $this->getProject()['apiKey']; - - $function = $this->client->call(Client::METHOD_POST, '/functions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'execute' => [Role::any()->toString()], 'runtime' => 'node-18.0', 'entrypoint' => 'index.js' ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $folder = 'node'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $apikey, - ], [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.js', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), //different tarball names intentional + 'code' => $this->packageFunction('node'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -859,23 +357,13 @@ class FunctionsCustomClientTest extends Scope 'x-appwrite-user-id' => "OVERRIDDEN", 'x-appwrite-user-jwt' => "OVERRIDDEN", ]); - $output = json_decode($execution['body']['responseBody'], true); $this->assertNotEquals('OVERRIDDEN', $output['APPWRITE_FUNCTION_JWT']); $this->assertNotEquals('OVERRIDDEN', $output['APPWRITE_FUNCTION_EVENT']); $this->assertNotEquals('OVERRIDDEN', $output['APPWRITE_FUNCTION_TRIGGER']); $this->assertNotEquals('OVERRIDDEN', $output['APPWRITE_FUNCTION_USER_ID']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); - - return []; + $this->cleanupFunction($functionId); } public function testListTemplates() @@ -883,50 +371,43 @@ class FunctionsCustomClientTest extends Scope /** * Test for SUCCESS */ - $expectedTemplates = array_slice(Config::getParam('function-templates', []), 0, 25); + // List all templates $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', ], $this->getHeaders())); - $this->assertEquals(200, $templates['headers']['status-code']); $this->assertGreaterThan(0, $templates['body']['total']); $this->assertIsArray($templates['body']['templates']); - - $this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]); - $this->assertArrayHasKey('useCases', $templates['body']['templates'][0]); - for ($i = 0; $i < 25; $i++) { - $this->assertEquals($expectedTemplates[$i]['name'], $templates['body']['templates'][$i]['name']); - $this->assertEquals($expectedTemplates[$i]['id'], $templates['body']['templates'][$i]['id']); - $this->assertEquals($expectedTemplates[$i]['icon'], $templates['body']['templates'][$i]['icon']); - $this->assertEquals($expectedTemplates[$i]['tagline'], $templates['body']['templates'][$i]['tagline']); - $this->assertEquals($expectedTemplates[$i]['useCases'], $templates['body']['templates'][$i]['useCases']); - $this->assertEquals($expectedTemplates[$i]['vcsProvider'], $templates['body']['templates'][$i]['vcsProvider']); - $this->assertEquals($expectedTemplates[$i]['runtimes'], $templates['body']['templates'][$i]['runtimes']); - $this->assertEquals($expectedTemplates[$i]['variables'], $templates['body']['templates'][$i]['variables']); - if (array_key_exists('scopes', $expectedTemplates[$i])) { - $this->assertEquals($expectedTemplates[$i]['scopes'], $templates['body']['templates'][$i]['scopes']); - } + foreach ($templates['body']['templates'] as $template) { + $this->assertArrayHasKey('name', $template); + $this->assertArrayHasKey('id', $template); + $this->assertArrayHasKey('icon', $template); + $this->assertArrayHasKey('tagline', $template); + $this->assertArrayHasKey('useCases', $template); + $this->assertArrayHasKey('vcsProvider', $template); + $this->assertArrayHasKey('runtimes', $template); + $this->assertArrayHasKey('variables', $template); + $this->assertArrayHasKey('scopes', $template); } - $templates_offset = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ + // List templates with pagination + $templatesOffset = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', ], $this->getHeaders()), [ 'limit' => 1, 'offset' => 2 ]); + $this->assertEquals(200, $templatesOffset['headers']['status-code']); + $this->assertEquals(1, $templatesOffset['body']['total']); + $this->assertEquals($templates['body']['templates'][2]['id'], $templatesOffset['body']['templates'][0]['id']); - $this->assertEquals(200, $templates_offset['headers']['status-code']); - $this->assertEquals(1, $templates_offset['body']['total']); - // assert that offset works as expected - $this->assertEquals($templates['body']['templates'][2]['id'], $templates_offset['body']['templates'][0]['id']); - + // List templates with filters $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', ], $this->getHeaders()), [ 'useCases' => ['starter', 'ai'], 'runtimes' => ['bun-1.0', 'dart-2.16'] ]); - $this->assertEquals(200, $templates['headers']['status-code']); $this->assertGreaterThanOrEqual(3, $templates['body']['total']); $this->assertIsArray($templates['body']['templates']); @@ -936,6 +417,7 @@ class FunctionsCustomClientTest extends Scope $this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]); $this->assertContains('bun-1.0', array_column($templates['body']['templates'][0]['runtimes'], 'name')); + // List templates with pagination and filters $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -958,16 +440,16 @@ class FunctionsCustomClientTest extends Scope /** * Test for FAILURE */ + // List templates with invalid limit $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', ], $this->getHeaders()), [ 'limit' => 5001, 'offset' => 10, ]); - $this->assertEquals(400, $templates['headers']['status-code']); - $this->assertEquals('Invalid `limit` param: Value must be a valid range between 1 and 5,000', $templates['body']['message']); + // List templates with invalid offset $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -975,9 +457,7 @@ class FunctionsCustomClientTest extends Scope 'limit' => 5, 'offset' => 5001, ]); - $this->assertEquals(400, $templates['headers']['status-code']); - $this->assertEquals('Invalid `offset` param: Value must be a valid range between 0 and 5,000', $templates['body']['message']); } public function testGetTemplate() @@ -985,10 +465,7 @@ class FunctionsCustomClientTest extends Scope /** * Test for SUCCESS */ - $template = $this->client->call(Client::METHOD_GET, '/functions/templates/query-neo4j-auradb', array_merge([ - 'content-type' => 'application/json', - ], $this->getHeaders()), []); - + $template = $this->getTemplate('query-neo4j-auradb'); $this->assertEquals(200, $template['headers']['status-code']); $this->assertIsArray($template['body']); $this->assertEquals('query-neo4j-auradb', $template['body']['id']); @@ -997,15 +474,13 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('Graph database with focus on relations between data.', $template['body']['tagline']); $this->assertEquals(['databases'], $template['body']['useCases']); $this->assertEquals('github', $template['body']['vcsProvider']); + $this->assertIsArray($template['body']['runtimes']); + $this->assertIsArray($template['body']['scopes']); /** * Test for FAILURE */ - $template = $this->client->call(Client::METHOD_GET, '/functions/templates/invalid-template-id', array_merge([ - 'content-type' => 'application/json', - ], $this->getHeaders()), []); - + $template = $this->getTemplate('invalid-template-id'); $this->assertEquals(404, $template['headers']['status-code']); - $this->assertEquals('Function Template with the requested ID could not be found.', $template['body']['message']); } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 193e99d89e..8fe4aedca7 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -388,12 +388,9 @@ class FunctionsCustomServerTest extends Scope public function testCreateDeploymentFromCLI() { - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function = $this->createFunction([ 'functionId' => ID::unique(), - 'name' => 'Test', + 'name' => 'test-cli-deployment', 'execute' => [Role::user($this->getUser()['$id'])->toString()], 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', @@ -401,210 +398,127 @@ class FunctionsCustomServerTest extends Scope 'users.*.create', 'users.*.delete', ], - 'schedule' => '0 0 1 1 *', + 'schedule' => '0 0 1 1 *', // Once a year 'timeout' => 10, ]); + $functionId = $function['body']['$id']; $this->assertEquals(201, $function['headers']['status-code']); - $functionId = $function['body']['$id']; - - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/deployments', [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - 'x-sdk-language' => 'cli', - ], [ + $deployment = $this->createDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true - ]); - - $deploymentId = $deployment['body']['$id'] ?? ''; + ], cli: true); + $deploymentId = $deployment['body']['$id']; $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertDeploymentStatus($functionId, $deploymentId, 'ready'); - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $functionDetails = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(200, $functionDetails['headers']['status-code']); - $this->assertEquals('cli', $functionDetails['body']['type']); + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('cli', $deployment['body']['type']); } public function testCreateDeploymentFromTemplate() { - $runtimeName = 'php-8.0'; - // Fetch starter template (used to create function later) - $template = $this->client->call(Client::METHOD_GET, '/functions/templates/starter', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $this->assertEquals(200, $template['headers']['status-code']); + $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'php-8.0'; + }))[0]; - $entrypoint = null; - $rootDirectory = null; - $commands = null; - foreach ($template['body']['runtimes'] as $runtime) { - if ($runtime["name"] !== $runtimeName) { - continue; - } - - $entrypoint = $runtime["entrypoint"]; - $rootDirectory = $runtime["providerRootDirectory"]; - $commands = $runtime["commands"]; - break; - } - - $this->assertNotNull($entrypoint); - - /** - * If below test ever starts failing, it means temaplate used in - * this test now has some variables. This test currently doesnt test variables. - * Remove bellow assertion and update test to crete variable, - * and ensure variable works as expected in execution. - */ - $this->assertEmpty($template['body']['variables']); - - // Create function using settings from template. - // Deployment is automatically created from template inside endpoint - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], $this->getHeaders()), [ - 'functionId' => ID::unique(), - 'name' => $template['body']['name'], - 'runtime' => $runtimeName, - 'execute' => $template['body']['permissions'], - 'entrypoint' => $entrypoint, - 'events' => $template['body']['events'], - 'schedule' => $template['body']['cron'], - 'timeout' => $template['body']['timeout'], - 'commands' => $commands, - 'scopes' => $template['body']['scopes'], - 'templateRepository' => $template['body']['providerRepositoryId'], - 'templateOwner' => $template['body']['providerOwner'], - 'templateRootDirectory' => $rootDirectory, - 'templateVersion' => $template['body']['providerVersion'], - ]); + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'], + 'runtime' => 'php-8.0', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $phpRuntime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $phpRuntime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + 'templateRepository' => $starterTemplate['body']['providerRepositoryId'], + 'templateOwner' => $starterTemplate['body']['providerOwner'], + 'templateRootDirectory' => $phpRuntime['providerRootDirectory'], + 'templateVersion' => $starterTemplate['body']['providerVersion'], + ] + ); + $functionId = $function['body']['$id']; $this->assertEquals(201, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); - $functionId = $function['body']['$id']; - - // List deployments so we can await deployment build - $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - + $deployments = $this->listDeployments($functionId); $this->assertEquals(200, $deployments['headers']['status-code']); $this->assertEquals(1, $deployments['body']['total']); - $this->assertNotEmpty($deployments['body']['deployments'][0]['$id']); - $this->assertEquals(0, $deployments['body']['deployments'][0]['size']); - $deploymentId = $deployments['body']['deployments'][0]['$id']; + $lastDeployment = $deployments['body']['deployments'][0]; + $this->assertNotEmpty($lastDeployment['$id']); + $this->assertEquals(0, $lastDeployment['size']); - // Wait for deployment build to finish - // Deployment is automatically activated - $this->assertDeployment($function['body']['$id'], $deploymentId); + $deploymentId = $lastDeployment['$id']; + $this->assertDeploymentStatus($function['body']['$id'], $deploymentId, 'ready'); - $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - $this->assertGreaterThan(0, $deployments['body']['size']); - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], $this->getHeaders()), []); + $deployment = $this->getDeployment($function['body']['$id'], $deploymentId); + $this->assertGreaterThan(0, $deployment['body']['size']); + $function = $this->getFunction($functionId); $this->assertEquals(200, $function['headers']['status-code']); $this->assertEquals($deploymentId, $function['body']['deployment']); - // Execute function to ensure starter code is used - // Also tests if dynamic keys works as expected - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'path' => '/ping' + // Test starter code is used and that dynamic keys work + $execution = $this->createExecution($functionId, [ + 'path' => '/ping', ]); - $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals("completed", $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); $this->assertEquals("Pong", $execution['body']['responseBody']); $this->assertEmpty($execution['body']['errors']); - // Get users to ensure execution logged correct total users + // Test execution logged correct total users $users = $this->client->call(Client::METHOD_GET, '/users', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], $this->getHeaders()), []); - $this->assertEquals(200, $users['headers']['status-code']); $this->assertIsInt($users['body']['total']); - $totalusers = $users['body']['total']; - - $this->assertStringContainsString("Total users: " . $totalusers, $execution['body']['logs']); + $totalUsers = $users['body']['total']; + $this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']); // Execute function again but async - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'path' => '/ping', 'async' => true ]); - + $executionId = $execution['body']['$id']; $this->assertEquals(202, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); $this->assertEquals('waiting', $execution['body']['status']); - $executionId = $execution['body']['$id']; - // Wait for async execuntion to finish - sleep(5); + $this->assertEventually(function () use ($functionId, $executionId, $totalUsers) { + $execution = $this->getExecution($functionId, $executionId); - // Ensure execution was successful - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals("completed", $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEmpty($execution['body']['responseBody']); + $this->assertEmpty($execution['body']['errors']); + $this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']); + }, 100000, 250); - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals("completed", $execution['body']['status']); - $this->assertEquals(200, $execution['body']['responseStatusCode']); - $this->assertEmpty($execution['body']['responseBody']); - $this->assertEmpty($execution['body']['errors']); - $this->assertStringContainsString("Total users: " . $totalusers, $execution['body']['logs']); - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $function = $this->deleteFunction($functionId); + $this->assertEquals(204, $function['headers']['status-code']); } /** @@ -615,15 +529,11 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true ]); @@ -640,7 +550,7 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => 'false' ]); @@ -675,16 +585,11 @@ class FunctionsCustomServerTest extends Scope */ public function testCancelDeploymentBuild($data): void { - // Create a new deployment to cancel - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true ]); @@ -1406,16 +1311,6 @@ class FunctionsCustomServerTest extends Scope $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(202, $execution['headers']['status-code']); - $this->assertEventually(function () use ($data, $executionId) { - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]); - - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals('waiting', $execution['body']['status']); - }, 10000, 500); - $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1580,87 +1475,41 @@ class FunctionsCustomServerTest extends Scope public function testTimeout() { - $name = 'php-8.0'; - $entrypoint = 'index.php'; - $timeout = 15; - $folder = 'timeout'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'functionId' => ID::unique(), - 'name' => 'Test ' . $name, - 'runtime' => $name, - 'entrypoint' => $entrypoint, + $function = $this->createFunction([ + 'name' => 'timeout-php-8.0', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', 'events' => [], - 'schedule' => '* * * * *', // execute every minute - 'timeout' => $timeout, + 'schedule' => '*/10 * * * *', // Execute every 10 seconds + 'timeout' => 15, ]); - - $functionId = $function['body']['$id'] ?? ''; - + $functionId = $function['body']['id']; $this->assertEquals(201, $function['headers']['status-code']); - $this->assertEquals('* * * * *', $function['body']['schedule']); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'entrypoint' => $entrypoint, - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), - 'activate' => true, - ]); - - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($functionId, $deploymentId); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'async' => true, - ]); + $deployment = $this->createDeployment($functionId, 'php'); + $deploymentId = $deployment['body']['$id']; + $this->assertDeploymentStatus($functionId, $deploymentId, 'ready'); + $execution = $this->createExecution($functionId, ['async' => true]); $executionId = $execution['body']['$id'] ?? ''; - $this->assertEquals(202, $execution['headers']['status-code']); - $executionData = null; + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); - $this->assertEventually(function () use ($functionId, $executionId, &$executionData) { - $executionData = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals('failed', $execution['body']['status']); + $this->assertEquals('failed', $execution['body']['status']); + $this->assertEquals(500, $execution['body']['responseStatusCode']); + $this->assertGreaterThan(2, $execution['body']['duration']); + $this->assertLessThan(20, $execution['body']['duration']); + $this->assertEquals('', $execution['body']['responseBody']); + $this->assertEquals('', $execution['body']['logs']); + $this->assertStringContainsString('timed out', $execution['body']['errors']); + }, 20000, 500); - $this->assertEquals(200, $executionData['headers']['status-code']); - $this->assertEquals('failed', $executionData['body']['status']); - - if ($executionData['body']['status'] !== 'failed') { - } - }, 30000, 1000); - - $this->assertEquals('failed', $executionData['body']['status']); - $this->assertEquals(500, $executionData['body']['responseStatusCode']); - $this->assertGreaterThan(2, $executionData['body']['duration']); - $this->assertLessThan(20, $executionData['body']['duration']); - $this->assertEquals('', $executionData['body']['responseBody']); - $this->assertEquals('', $executionData['body']['logs']); - $this->assertStringContainsString('timed out', $executionData['body']['errors']); - - $scheduledExecutionData = null; - - $this->assertEventually(function () use ($functionId, &$scheduledExecutionData) { - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->assertEventually(function () use ($functionId) { + $executions = $this->listExecutions($functionId, [ 'queries' => [ Query::equal('trigger', ['schedule'])->toString(), ], @@ -1669,19 +1518,12 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $executions['headers']['status-code']); $this->assertEquals(1, $executions['body']['total']); - $scheduledExecutionData = $executions['body']['executions'][0]; - }, 140000, 1000); // Wait up to 140 seconds, checking every 1 second + $lastExecution = $executions['body']['executions'][0]; + $this->assertEquals('schedule', $lastExecution['trigger']); + }, 20000, 500); - $this->assertEquals('schedule', $scheduledExecutionData['trigger']); - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $function = $this->deleteFunction($functionId); + $this->assertEquals(204, $function['headers']['status-code']); } From af509f3479a25def65685e090d33bf4ee6cd41f1 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:17:38 +0100 Subject: [PATCH 135/241] fix: flakyness --- .../e2e/Services/Functions/FunctionsBase.php | 78 +++++++------------ .../Functions/FunctionsConsoleClientTest.php | 24 ++++-- .../Functions/FunctionsCustomClientTest.php | 10 ++- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 92210a3027..cdb4781e5e 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -21,27 +21,26 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $params); - $functionId = $function['body']['$id']; + $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'])); - $this->assertEquals($function['headers']['status-code'], 201); + $functionId = $function['body']['$id']; return $functionId; } protected function setupDeployment(string $functionId, mixed $params): string { $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'application/json', + 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $params); + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'])); $deploymentId = $deployment['body']['$id']; - $this->assertEquals($deployment['headers']['status-code'], 202); - $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals('completed', $deployment['body']['status']); - }, 200000, 500); + }, 50000, 500); return $deploymentId; } @@ -57,40 +56,27 @@ trait FunctionsBase $this->assertEquals($function['headers']['status-code'], 204); } - protected function createFunction(mixed $params) + protected function createFunction(mixed $params): mixed { $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), $params); - $functionId = $function['body']['$id']; - - if (empty($variables)) { - return $function; - } - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); return $function; } - protected function createVariable($functionId, $key, $value) + protected function createVariable(string $functionId, mixed $params): mixed { $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => $key, - 'value' => $value - ]); + ], $this->getHeaders()), $params); return $variable; } - protected function getFunction($functionId) + protected function getFunction(string $functionId): mixed { $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ 'content-type' => 'application/json', @@ -100,7 +86,7 @@ trait FunctionsBase return $function; } - protected function getDeployment($functionId, $deploymentId) + protected function getDeployment(string $functionId, string $deploymentId): mixed { $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', @@ -120,12 +106,22 @@ trait FunctionsBase return $execution; } + protected function listFunctions($params = []) + { + $functions = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $functions; + } + protected function listDeployments($functionId, $params = []) { $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), $params)); + ], $this->getHeaders()), $params); return $deployments; } @@ -135,19 +131,17 @@ trait FunctionsBase $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), $params)); + ], $this->getHeaders()), $params); return $executions; } - protected function packageFunction(string $folder = 'php') + protected function packageFunction(string $folder = 'php'): CURLFile { $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); - if (!file_exists($code)) { - throw new \Exception('Failed to create code package. ' . $code . ' does not exist.'); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); } if (filesize($code) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); @@ -158,36 +152,22 @@ trait FunctionsBase protected function createDeployment( string $functionId, - mixed $params, - bool $cli = false, - bool $admin = false + mixed $params ) { - $authHeaders = $this->getHeaders(); - - if ($admin) { - $authHeaders = [ - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]; - } - if ($cli) { - $authHeaders[] = 'x-sdk-language: cli'; - } - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], - - ], $params); + ], $this->getHeaders()), $params); return $deployment; } - protected function getFunctionUsage($functionId, $params) + protected function getFunctionUsage(string $functionId, mixed $params): mixed { $usage = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), $params)); + ], $this->getHeaders()), $params); return $usage; } diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 6ee5a1085d..cbfca09564 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -105,8 +105,10 @@ class FunctionsConsoleClientTest extends Scope */ $variable = $this->createVariable( $data['functionId'], - 'APP_TEST', - 'TESTINGVALUE' + [ + 'key' => 'APP_TEST', + 'value' => 'TESTINGVALUE' + ] ); $variableId = $variable['body']['$id']; $this->assertEquals(201, $variable['headers']['status-code']); @@ -117,24 +119,30 @@ class FunctionsConsoleClientTest extends Scope // Test for duplicate key $variable = $this->createVariable( $data['functionId'], - 'APP_TEST', - 'ANOTHER_TESTINGVALUE' + [ + 'key' => 'APP_TEST', + 'value' => 'ANOTHERTESTINGVALUE' + ] ); $this->assertEquals(409, $variable['headers']['status-code']); // Test for invalid key $variable = $this->createVariable( $data['functionId'], - str_repeat("A", 256), - 'TESTINGVALUE' + [ + 'key' => str_repeat("A", 256), + 'value' => 'TESTINGVALUE' + ] ); $this->assertEquals(400, $variable['headers']['status-code']); // Test for invalid value $variable = $this->createVariable( $data['functionId'], - 'LONGKEY', - str_repeat("#", 8193) + [ + 'key' => 'LONGKEY', + 'value' => str_repeat("#", 8193), + ] ); $this->assertEquals(400, $variable['headers']['status-code']); diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 872249085e..1dc81c8171 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -15,7 +15,7 @@ class FunctionsCustomClientTest extends Scope use ProjectCustom; use SideClient; - public function testCreate() + public function testCreateFunction() { /** * Test for FAILURE @@ -80,7 +80,7 @@ class FunctionsCustomClientTest extends Scope $this->assertNotEmpty($asyncExecution['logs']); $this->assertNotEmpty($asyncExecution['errors']); $this->assertGreaterThan(0, $asyncExecution['duration']); - }, 100000, 250); + }, 10000, 250); $this->cleanupFunction($functionId); } @@ -106,7 +106,7 @@ class FunctionsCustomClientTest extends Scope // Schedule execution for the future \date_default_timezone_set('UTC'); - $futureTime = (new \DateTime())->add(new \DateInterval('PT30S')); // 30 seconds from now + $futureTime = (new \DateTime())->add(new \DateInterval('PT15S')); // 15 seconds from now $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); $execution = $this->createExecution($functionId, [ @@ -126,6 +126,8 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals('/custom-path', $execution['body']['requestPath']); $this->assertCount(0, $execution['body']['requestHeaders']); + \sleep(15); + $this->assertEventually(function () use ($functionId, $executionId) { $execution = $this->getExecution($functionId, $executionId); $this->assertEquals('completed', $execution['body']['status']); @@ -141,7 +143,7 @@ class FunctionsCustomClientTest extends Scope $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); $this->assertGreaterThan(0, $execution['body']['duration']); - }, 40000, 2500); + }, 10000, 250); /* Test for FAILURE */ // Schedule synchronous execution From d6d7cf1d9fbaec70274b2dbcb6b143cbd7061730 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 19 Sep 2024 18:27:02 +0300 Subject: [PATCH 136/241] Pull 1.6.x --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index c0f3333469..079c8d749f 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -151,7 +151,7 @@ abstract class ScheduleBase extends Action $total = $total + $sum; foreach ($results as $document) { - $localDocument = $schedules[$document['resourceId']] ?? null; + $localDocument = $this->schedules[$document['resourceId']] ?? null; // Check if resource has been updated since last sync $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; From 9a0c72294c19ab9d7f84c27befed05f690bb5408 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 19 Sep 2024 18:37:09 +0300 Subject: [PATCH 137/241] composer.lock --- composer.lock | 62 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index 147800df32..d2a5d88fb9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b6820da26239716cf14a445697902a03", + "content-hash": "67946bb84bca00b694c8a41fbe41f359", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -2174,27 +2175,35 @@ }, { "name": "utopia-php/migration", - "version": "0.5.2", + "version": "dev-preserveDates", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a" + "reference": "c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f18d44d4459f78c292dac9edde856fd156fe497a", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe", + "reference": "c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "11.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3.*", + "utopia-php/database": "0.53.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "0.33.*", + "utopia-php/storage": "0.18.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", - "vlucas/phpdotenv": "5.*" + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.16.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2216,9 +2225,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.2" + "source": "https://github.com/utopia-php/migration/tree/preserveDates" }, - "time": "2024-07-22T09:27:07+00:00" + "time": "2024-09-19T15:32:44+00:00" }, { "name": "utopia-php/mongo", @@ -6993,9 +7002,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/migration", + "version": "dev-preserveDates", + "alias": "0.6.0", + "alias_normalized": "0.6.0.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From 964b616e05b3c6ee2c1ba3b03b276deb95c59ee8 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:21:00 +0100 Subject: [PATCH 138/241] fix: refactor custom server test --- .../e2e/Services/Functions/FunctionsBase.php | 16 +- .../Functions/FunctionsCustomClientTest.php | 37 +- .../Functions/FunctionsCustomServerTest.php | 1582 +++++------------ 3 files changed, 481 insertions(+), 1154 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index cdb4781e5e..730c7bff48 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -21,7 +21,7 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $params); - $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'])); + $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); $functionId = $function['body']['$id']; return $functionId; @@ -34,12 +34,16 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $params); - $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'])); - $deploymentId = $deployment['body']['$id']; + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEventually(function () use ($functionId, $deploymentId) { - $deployment = $this->getDeployment($functionId, $deploymentId); - $this->assertEquals('completed', $deployment['body']['status']); + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals('ready', $deployment['body']['status']); }, 50000, 500); return $deploymentId; @@ -182,7 +186,7 @@ trait FunctionsBase return $template; } - protected function createExecution(string $functionId, mixed $params) + protected function createExecution(string $functionId, mixed $params = []) { $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 1dc81c8171..dd950ca655 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -48,7 +48,7 @@ class FunctionsCustomClientTest extends Scope 'users.*.create', 'users.*.delete', ], - 'schedule' => '* * * * *', // Execute every minute + 'schedule' => '* * * * *', // Execute every 60 seconds 'timeout' => 10, ]); $this->setupDeployment($functionId, [ @@ -57,18 +57,30 @@ class FunctionsCustomClientTest extends Scope 'activate' => true ]); - $execution = $this->createExecution($functionId, [ - 'async' => false, + // Deny create async execution as guest + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'async' => true, ]); $this->assertEquals(401, $execution['headers']['status-code']); + // Allow create async execution as user $execution = $this->createExecution($functionId, [ 'async' => true, ]); $this->assertEquals(202, $execution['headers']['status-code']); + // Wait for scheduled execution + \sleep(60); + $this->assertEventually(function () use ($functionId) { - $executions = $this->listExecutions($functionId); + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['key'], + ]); $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(2, $executions['body']['executions']); @@ -80,7 +92,7 @@ class FunctionsCustomClientTest extends Scope $this->assertNotEmpty($asyncExecution['logs']); $this->assertNotEmpty($asyncExecution['errors']); $this->assertGreaterThan(0, $asyncExecution['duration']); - }, 10000, 250); + }, 10000, 500); $this->cleanupFunction($functionId); } @@ -97,6 +109,7 @@ class FunctionsCustomClientTest extends Scope 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'timeout' => 10, + 'logging' => true, ]); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -106,7 +119,7 @@ class FunctionsCustomClientTest extends Scope // Schedule execution for the future \date_default_timezone_set('UTC'); - $futureTime = (new \DateTime())->add(new \DateInterval('PT15S')); // 15 seconds from now + $futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); // 2 minute in the future $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); $execution = $this->createExecution($functionId, [ @@ -120,17 +133,18 @@ class FunctionsCustomClientTest extends Scope ] ]); $executionId = $execution['body']['$id']; + $this->assertEquals(202, $execution['headers']['status-code']); $this->assertEquals('scheduled', $execution['body']['status']); $this->assertEquals('PATCH', $execution['body']['requestMethod']); $this->assertEquals('/custom-path', $execution['body']['requestPath']); $this->assertCount(0, $execution['body']['requestHeaders']); - \sleep(15); + \sleep(120); $this->assertEventually(function () use ($functionId, $executionId) { $execution = $this->getExecution($functionId, $executionId); - $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['headers']['status-code']); $this->assertEquals(200, $execution['body']['responseStatusCode']); $this->assertEquals('completed', $execution['body']['status']); @@ -143,7 +157,7 @@ class FunctionsCustomClientTest extends Scope $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); $this->assertGreaterThan(0, $execution['body']['duration']); - }, 10000, 250); + }, 10000, 500); /* Test for FAILURE */ // Schedule synchronous execution @@ -377,9 +391,11 @@ class FunctionsCustomClientTest extends Scope $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ 'content-type' => 'application/json', ], $this->getHeaders())); + $this->assertEquals(200, $templates['headers']['status-code']); $this->assertGreaterThan(0, $templates['body']['total']); $this->assertIsArray($templates['body']['templates']); + foreach ($templates['body']['templates'] as $template) { $this->assertArrayHasKey('name', $template); $this->assertArrayHasKey('id', $template); @@ -389,7 +405,6 @@ class FunctionsCustomClientTest extends Scope $this->assertArrayHasKey('vcsProvider', $template); $this->assertArrayHasKey('runtimes', $template); $this->assertArrayHasKey('variables', $template); - $this->assertArrayHasKey('scopes', $template); } // List templates with pagination @@ -434,9 +449,11 @@ class FunctionsCustomClientTest extends Scope $this->assertEquals(5, $templates['body']['total']); $this->assertIsArray($templates['body']['templates']); $this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]); + foreach ($templates['body']['templates'] as $template) { $this->assertContains($template['useCases'][0], ['databases']); } + $this->assertContains('node-16.0', array_column($templates['body']['templates'][0]['runtimes'], 'name')); /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8fe4aedca7..80f34d6d34 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -10,27 +10,27 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; use Utopia\App; +use Utopia\CLI\Console; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use function PHPSTORM_META\map; + class FunctionsCustomServerTest extends Scope { use FunctionsBase; use ProjectCustom; use SideServer; - public function testCreate(): array + public function testCreateFunction(): array { /** * Test for SUCCESS */ - $response1 = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function = $this->createFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'runtime' => 'php-8.0', @@ -42,162 +42,115 @@ class FunctionsCustomServerTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); - - $functionId = $response1['body']['$id'] ?? ''; - - $this->assertEquals(201, $response1['headers']['status-code']); - $this->assertNotEmpty($response1['body']['$id']); - $this->assertEquals('Test', $response1['body']['name']); - $this->assertEquals('php-8.0', $response1['body']['runtime']); + $functionId = $functionId = $function['body']['$id'] ?? ''; $dateValidator = new DatetimeValidator(); - $this->assertEquals(true, $dateValidator->isValid($response1['body']['$createdAt'])); - $this->assertEquals(true, $dateValidator->isValid($response1['body']['$updatedAt'])); - $this->assertEquals('', $response1['body']['deployment']); + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + $this->assertEquals('Test', $function['body']['name']); + $this->assertEquals('php-8.0', $function['body']['runtime']); + $this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt'])); + $this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt'])); + $this->assertEquals('', $function['body']['deployment']); $this->assertEquals([ 'buckets.*.create', 'buckets.*.delete', - ], $response1['body']['events']); - $this->assertEquals('0 0 1 1 *', $response1['body']['schedule']); - $this->assertEquals(10, $response1['body']['timeout']); + ], $function['body']['events']); + $this->assertEquals('0 0 1 1 *', $function['body']['schedule']); + $this->assertEquals(10, $function['body']['timeout']); - /** Create Variables */ - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $variable = $this->createVariable($functionId, [ 'key' => 'funcKey1', 'value' => 'funcValue1', ]); - - $variable2 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $variable2 = $this->createVariable($functionId, [ 'key' => 'funcKey2', 'value' => 'funcValue2', ]); - - $variable3 = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $variable3 = $this->createVariable($functionId, [ 'key' => 'funcKey3', 'value' => 'funcValue3', ]); - $this->assertEquals(201, $variable['headers']['status-code']); $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertEquals(201, $variable3['headers']['status-code']); - /** - * Test for FAILURE - */ - return [ 'functionId' => $functionId, ]; } /** - * @depends testCreate + * @depends testCreateFunction */ - public function testList(array $data): array + public function testListFunctions(array $data): array { /** * Test for SUCCESS */ - - /** - * Test search queries - */ - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test search id + $functions = $this->listFunctions([ 'search' => $data['functionId'] ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); + $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - $this->assertEquals($response['body']['functions'][0]['name'], 'Test'); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test pagination limit + $functions = $this->listFunctions([ 'queries' => [ Query::limit(1)->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test pagination offset + $functions = $this->listFunctions([ 'queries' => [ Query::offset(1)->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(0, $functions['body']['functions']); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(0, $response['body']['functions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test filter enabled + $functions = $this->listFunctions([ 'queries' => [ Query::equal('enabled', [true])->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test filter disabled + $functions = $this->listFunctions([ 'queries' => [ Query::equal('enabled', [false])->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(0, $functions['body']['functions']); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(0, $response['body']['functions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test search name + $functions = $this->listFunctions([ 'search' => 'Test' ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); + $this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - $this->assertEquals($response['body']['functions'][0]['$id'], $data['functionId']); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test search runtime + $functions = $this->listExecutions([ 'search' => 'php-8.0' ]); - - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - $this->assertEquals($response['body']['functions'][0]['$id'], $data['functionId']); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); + $this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']); /** * Test pagination */ - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test 2', 'runtime' => 'php-8.0', @@ -209,42 +162,8 @@ class FunctionsCustomServerTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); - $this->assertNotEmpty($response['body']['$id']); - - /** Create Variables */ - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $response['body']['$id'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'funcKey1', - 'value' => 'funcValue1', - ]); - - $variable2 = $this->client->call(Client::METHOD_POST, '/functions/' . $response['body']['$id'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'funcKey2', - 'value' => 'funcValue2', - ]); - - $variable3 = $this->client->call(Client::METHOD_POST, '/functions/' . $response['body']['$id'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'funcKey3', - 'value' => 'funcValue3', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - $this->assertEquals(201, $variable2['headers']['status-code']); - $this->assertEquals(201, $variable3['headers']['status-code']); - - $functions = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $functions = $this->listFunctions(); $this->assertEquals($functions['headers']['status-code'], 200); $this->assertEquals($functions['body']['total'], 2); $this->assertIsArray($functions['body']['functions']); @@ -252,87 +171,67 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); $this->assertEquals($functions['body']['functions'][1]['name'], 'Test 2'); - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functions = $this->listFunctions([ 'queries' => [ Query::cursorAfter(new Document(['$id' => $functions['body']['functions'][0]['$id']]))->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); + $this->assertEquals($functions['body']['functions'][0]['name'], 'Test 2'); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - $this->assertEquals($response['body']['functions'][0]['name'], 'Test 2'); - - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functions = $this->listFunctions([ 'queries' => [ Query::cursorBefore(new Document(['$id' => $functions['body']['functions'][1]['$id']]))->toString(), ], ]); - - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertCount(1, $response['body']['functions']); - $this->assertEquals($response['body']['functions'][0]['name'], 'Test'); + $this->assertEquals($functions['headers']['status-code'], 200); + $this->assertCount(1, $functions['body']['functions']); + $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functions = $this->listFunctions([ 'queries' => [ Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), ], ]); - - $this->assertEquals($response['headers']['status-code'], 400); + $this->assertEquals($functions['headers']['status-code'], 400); return $data; } /** - * @depends testList + * @depends testListFunctions */ - public function testGet(array $data): array + public function testGetFunction(array $data): array { /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $function = $this->getFunction($data['functionId']); $this->assertEquals($function['headers']['status-code'], 200); $this->assertEquals($function['body']['name'], 'Test'); /** * Test for FAILURE */ - $function = $this->client->call(Client::METHOD_GET, '/functions/x', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $function = $this->getFunction('x'); $this->assertEquals($function['headers']['status-code'], 404); return $data; } /** - * @depends testGet + * @depends testGetFunction */ - public function testUpdate($data): array + public function testUpdateFunction($data): array { /** * Test for SUCCESS */ - $response1 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -347,36 +246,25 @@ class FunctionsCustomServerTest extends Scope 'entrypoint' => 'index.php', ]); - $this->assertEquals(200, $response1['headers']['status-code']); - $this->assertNotEmpty($response1['body']['$id']); - $this->assertEquals('Test1', $response1['body']['name']); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + $this->assertEquals('Test1', $function['body']['name']); $dateValidator = new DatetimeValidator(); - $this->assertEquals(true, $dateValidator->isValid($response1['body']['$createdAt'])); - $this->assertEquals(true, $dateValidator->isValid($response1['body']['$updatedAt'])); - $this->assertEquals('', $response1['body']['deployment']); + $this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt'])); + $this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt'])); + $this->assertEquals('', $function['body']['deployment']); $this->assertEquals([ 'users.*.update.name', 'users.*.update.email', - ], $response1['body']['events']); - $this->assertEquals('0 0 1 1 *', $response1['body']['schedule']); - $this->assertEquals(15, $response1['body']['timeout']); + ], $function['body']['events']); + $this->assertEquals('0 0 1 1 *', $function['body']['schedule']); + $this->assertEquals(15, $function['body']['timeout']); - /** - * Create global variable to test in execution later - */ - $headers = [ - 'content-type' => 'application/json', - 'origin' => 'http://localhost', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-mode' => 'admin', - ]; - - $variable = $this->client->call(Client::METHOD_POST, '/project/variables', $headers, [ + // Create a variable for later tests + $variable = $this->createVariable($data['functionId'], [ 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); - $this->assertEquals(201, $variable['headers']['status-code']); /** @@ -388,7 +276,7 @@ class FunctionsCustomServerTest extends Scope public function testCreateDeploymentFromCLI() { - $function = $this->createFunction([ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'test-cli-deployment', 'execute' => [Role::user($this->getUser()['$id'])->toString()], @@ -401,26 +289,29 @@ class FunctionsCustomServerTest extends Scope 'schedule' => '0 0 1 1 *', // Once a year 'timeout' => 10, ]); - $functionId = $function['body']['$id']; - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->createDeployment($functionId, [ + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-sdk-language' => 'cli', + ], $this->getHeaders()), [ 'entrypoint' => 'index.php', 'code' => $this->packageFunction('php'), 'activate' => true - ], cli: true); - $deploymentId = $deployment['body']['$id']; - + ]); + $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertDeploymentStatus($functionId, $deploymentId, 'ready'); - $deployment = $this->getDeployment($functionId, $deploymentId); - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('cli', $deployment['body']['type']); + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('cli', $deployment['body']['type']); + }, 500000, 1000); } - public function testCreateDeploymentFromTemplate() + public function testCreateFunctionAndDeploymentFromTemplate() { $starterTemplate = $this->getTemplate('starter'); @@ -451,7 +342,7 @@ class FunctionsCustomServerTest extends Scope 'templateVersion' => $starterTemplate['body']['providerVersion'], ] ); - $functionId = $function['body']['$id']; + $functionId = $functionId = $function['body']['$id'] ?? ''; $this->assertEquals(201, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); @@ -464,10 +355,12 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(0, $lastDeployment['size']); $deploymentId = $lastDeployment['$id']; - $this->assertDeploymentStatus($function['body']['$id'], $deploymentId, 'ready'); - - $deployment = $this->getDeployment($function['body']['$id'], $deploymentId); - $this->assertGreaterThan(0, $deployment['body']['size']); + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('cli', $deployment['body']['type']); + }, 500000, 1000); $function = $this->getFunction($functionId); $this->assertEquals(200, $function['headers']['status-code']); @@ -500,18 +393,16 @@ class FunctionsCustomServerTest extends Scope 'path' => '/ping', 'async' => true ]); - $executionId = $execution['body']['$id']; + $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(202, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); $this->assertEquals('waiting', $execution['body']['status']); $this->assertEventually(function () use ($functionId, $executionId, $totalUsers) { $execution = $this->getExecution($functionId, $executionId); - - $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals("completed", $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals('completed', $execution['body']['status']); $this->assertEmpty($execution['body']['responseBody']); $this->assertEmpty($execution['body']['errors']); $this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']); @@ -522,21 +413,18 @@ class FunctionsCustomServerTest extends Scope } /** - * @depends testUpdate + * @depends testUpdateFunction */ public function testCreateDeployment($data): array { /** * Test for SUCCESS */ - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $data['functionId']; + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); @@ -544,37 +432,31 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); - $this->assertDeployment($data['functionId'], $deploymentId); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => 'false' + 'activate' => false ]); - + $deploymentIdInactive = $deployment['body']['$id']; $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deploymentIdInactive = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId, $deploymentIdInactive) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('completed', $deployment['body']['status']); - $this->assertDeployment($data['functionId'], $deploymentIdInactive); - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $deployment = $this->getDeployment($functionId, $deploymentIdInactive); + $this->assertEquals('completed', $deployment['body']['status']); + }, 500000, 1000); + $function = $this->getFunction($functionId); $this->assertEquals(200, $function['headers']['status-code']); $this->assertEquals($deploymentId, $function['body']['deployment']); $this->assertNotEquals($deploymentIdInactive, $function['body']['deployment']); - $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentIdInactive, array_merge([ + $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/deployments/' . $deploymentIdInactive, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); - $this->assertEquals(204, $deployment['headers']['status-code']); return array_merge($data, ['deploymentId' => $deploymentId]); @@ -585,14 +467,11 @@ class FunctionsCustomServerTest extends Scope */ public function testCancelDeploymentBuild($data): void { - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $data['functionId']; + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => true + 'activate' => false ]); - $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); @@ -600,39 +479,28 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); - $this->assertEventually(function () use ($data, $deploymentId) { - $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('building', $deployment['body']['status']); - }, 60000, 1000); + }, 100000, 250); - // Cancel the deployment build - $cancel = $this->client->call(Client::METHOD_PATCH, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId . '/build', [ + // Cancel the deployment + $cancel = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/build', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - + ], $this->getHeaders())); $this->assertEquals(200, $cancel['headers']['status-code']); $this->assertEquals('canceled', $cancel['body']['status']); /** * Build worker still runs the build. - * 10s sleep gives worker enough time to finish build. + * 30s sleep gives worker enough time to finish build. * After build finished, it should still be canceled, not ready. */ - \sleep(10); - $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + \sleep(30); + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); } @@ -645,10 +513,11 @@ class FunctionsCustomServerTest extends Scope /** * Test for Large Code File SUCCESS */ + $functionId = $data['functionId']; $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); @@ -666,7 +535,7 @@ class FunctionsCustomServerTest extends Scope if (!empty($id)) { $headers['x-appwrite-id'] = $id; } - $largeTag = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge($headers, $this->getHeaders()), [ + $largeTag = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge($headers, $this->getHeaders()), [ 'entrypoint' => 'index.php', 'code' => $curlFile, 'activate' => true, @@ -685,19 +554,15 @@ class FunctionsCustomServerTest extends Scope $this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['size']); // ~7MB video file $deploymentSize = $largeTag['body']['size']; - $deploymentId = $largeTag['body']['$id']; - $this->assertDeployment($data['functionId'], $deploymentId, true); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals($deploymentSize, $response['body']['size']); - $this->assertGreaterThan(1024 * 1024 * 10, $response['body']['buildSize']); // ~7MB video file + 10MB sample file + $this->assertEventually(function () use ($functionId, $deploymentId, $deploymentSize) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals($deploymentSize, $deployment['body']['size']); + $this->assertGreaterThan(1024 * 1024 * 10, $deployment['body']['buildSize']); // ~7MB video file + 10MB sample file + }, 500000, 1000); return $data; } @@ -710,6 +575,8 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ + $dateValidator = new DatetimeValidator(); + $response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $data['functionId'] . '/deployments/' . $data['deploymentId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -717,15 +584,10 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); - $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); $this->assertEquals(true, $dateValidator->isValid($response['body']['$updatedAt'])); $this->assertEquals($data['deploymentId'], $response['body']['deployment']); - /** - * Test for FAILURE - */ - return $data; } @@ -737,127 +599,85 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $functionId = $data['functionId']; + $deployments = $this->listDeployments($functionId); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals($function['body']['total'], 3); - $this->assertIsArray($function['body']['deployments']); - $this->assertCount(3, $function['body']['deployments']); - $this->assertArrayHasKey('size', $function['body']['deployments'][0]); - $this->assertArrayHasKey('buildSize', $function['body']['deployments'][0]); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals($deployments['body']['total'], 3); + $this->assertIsArray($deployments['body']['deployments']); + $this->assertCount(3, $deployments['body']['deployments']); + $this->assertArrayHasKey('size', $deployments['body']['deployments'][0]); + $this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]); - /** - * Test search queries - */ - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), [ - 'search' => $data['functionId'] - ]) - ); + // Test search id + $deployments = $this->listDeployments($functionId, [ + 'search' => $data['functionId'] + ]); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); + $this->assertIsArray($deployments['body']['deployments']); + $this->assertCount(3, $deployments['body']['deployments']); + $this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); - $this->assertIsArray($function['body']['deployments']); - $this->assertCount(3, $function['body']['deployments']); - $this->assertEquals($function['body']['deployments'][0]['$id'], $data['deploymentId']); - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + // Test pagination limit + $deployments = $this->listDeployments($functionId, [ 'queries' => [ Query::limit(1)->toString(), ], ]); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertCount(1, $deployments['body']['deployments']); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertCount(1, $function['body']['deployments']); - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deployments = $this->listDeployments($functionId, [ 'queries' => [ Query::offset(1)->toString(), ], ]); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertCount(2, $function['body']['deployments']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertCount(2, $deployments['body']['deployments']); - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deployments = $this->listDeployments($functionId, [ 'queries' => [ Query::equal('entrypoint', ['index.php'])->toString(), ], ]); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertCount(3, $function['body']['deployments']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertCount(3, $deployments['body']['deployments']); - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deployments = $this->listDeployments($functionId, [ 'queries' => [ Query::equal('entrypoint', ['index.js'])->toString(), ], ]); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertCount(0, $function['body']['deployments']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertCount(0, $deployments['body']['deployments']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), [ - 'search' => 'Test' - ]) - ); + $deployments = $this->listDeployments($functionId, [ + 'search' => 'Test' + ]); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); - $this->assertIsArray($function['body']['deployments']); - $this->assertCount(3, $function['body']['deployments']); - $this->assertEquals($function['body']['deployments'][0]['$id'], $data['deploymentId']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders(), [ - 'search' => 'php-8.0' - ]) - ); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); + $this->assertIsArray($deployments['body']['deployments']); + $this->assertCount(3, $deployments['body']['deployments']); + $this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); - $this->assertIsArray($function['body']['deployments']); - $this->assertCount(3, $function['body']['deployments']); - $this->assertEquals($function['body']['deployments'][0]['$id'], $data['deploymentId']); + $deployments = $this->listDeployments($functionId, [ + 'search' => 'php-8.0' + ]); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); + $this->assertIsArray($deployments['body']['deployments']); + $this->assertCount(3, $deployments['body']['deployments']); + $this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']); + + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::equal('type', ['manual'])->toString(), @@ -865,16 +685,11 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::equal('type', ['vcs'])->toString(), @@ -882,16 +697,11 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(0, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(0, $deployments['body']['total']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::equal('type', ['invalid-string'])->toString(), @@ -899,16 +709,11 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(0, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(0, $deployments['body']['total']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::greaterThan('size', 10000)->toString(), @@ -916,16 +721,11 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(1, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(1, $deployments['body']['total']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::greaterThan('size', 0)->toString(), @@ -933,57 +733,40 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); - $function = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::greaterThan('size', -100)->toString(), ], ] ); - - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals(3, $function['body']['total']); + $this->assertEquals($deployments['headers']['status-code'], 200); + $this->assertEquals(3, $deployments['body']['total']); /** * Ensure size output and size filters work exactly. * Prevents buildSize being counted towards deployemtn size */ - $response = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ Query::limit(1)->toString(), ] ); + $this->assertEquals(200, $deployments['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $deployments['body']['total']); + $this->assertNotEmpty($deployments['body']['deployments'][0]['$id']); + $this->assertNotEmpty($deployments['body']['deployments'][0]['size']); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertGreaterThanOrEqual(1, $response['body']['total']); - $this->assertNotEmpty($response['body']['deployments'][0]['$id']); - $this->assertNotEmpty($response['body']['deployments'][0]['size']); + $deploymentId = $deployments['body']['deployments'][0]['$id']; + $deploymentSize = $deployments['body']['deployments'][0]['size']; - $deploymentId = $function['body']['deployments'][0]['$id']; - $deploymentSize = $function['body']['deployments'][0]['size']; - - $response = $this->client->call( - Client::METHOD_GET, - '/functions/' . $data['functionId'] . '/deployments', - array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), + $deployments = $this->listDeployments( + $functionId, [ 'queries' => [ Query::equal('size', [$deploymentSize])->toString(), @@ -991,20 +774,21 @@ class FunctionsCustomServerTest extends Scope ] ); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertGreaterThan(0, $response['body']['total']); + $this->assertEquals(200, $deployments['headers']['status-code']); + $this->assertGreaterThan(0, $deployments['body']['total']); - $found = false; - foreach ($response['body']['deployments'] as $deployment) { - if ($deployment['$id'] === $deploymentId) { - $found = true; - $this->assertEquals($deploymentSize, $deployment['size']); - break; - } + $matchingDeployment = array_filter( + $deployments['body']['deployments'], + fn($deployment) => $deployment['$id'] === $deploymentId + ); + + $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); + + if (!empty($matchingDeployment)) { + $deployment = reset($matchingDeployment); + $this->assertEquals($deploymentSize, $deployment['size']); } - $this->assertTrue($found); - return $data; } @@ -1016,27 +800,19 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $data['deploymentId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $function['headers']['status-code']); - $this->assertGreaterThan(0, $function['body']['buildTime']); - $this->assertNotEmpty($function['body']['status']); - $this->assertNotEmpty($function['body']['buildLogs']); - $this->assertArrayHasKey('size', $function['body']); - $this->assertArrayHasKey('buildSize', $function['body']); + $deployment = $this->getDeployment($data['functionId'], $data['deploymentId']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['buildTime']); + $this->assertNotEmpty($deployment['body']['status']); + $this->assertNotEmpty($deployment['body']['buildLogs']); + $this->assertArrayHasKey('size', $deployment['body']); + $this->assertArrayHasKey('buildSize', $deployment['body']); /** * Test for FAILURE */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/x', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals($function['headers']['status-code'], 404); + $deployment = $this->getDeployment($data['functionId'], 'x'); + $this->assertEquals($deployment['headers']['status-code'], 404); return $data; } @@ -1049,13 +825,9 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($data['functionId'], [ 'async' => false, ]); - $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(201, $execution['headers']['status-code']); @@ -1077,10 +849,7 @@ class FunctionsCustomServerTest extends Scope $this->assertNotEmpty($execution['body']['logs']); $this->assertLessThan(10, $execution['body']['duration']); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($data['functionId'], [ 'async' => false, 'path' => '/?code=400' ]); @@ -1092,6 +861,8 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); + + $execution = $this->createExecution($data['functionId']); $this->assertEquals(204, $execution['headers']['status-code']); return array_merge($data, ['executionId' => $executionId]); @@ -1105,82 +876,57 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $executions = $this->listExecutions($data['functionId']); + $this->assertEquals($executions['headers']['status-code'], 200); + $this->assertEquals($executions['body']['total'], 1); + $this->assertIsArray($executions['body']['executions']); + $this->assertCount(1, $executions['body']['executions']); + $this->assertEquals($executions['body']['executions'][0]['$id'], $data['executionId']); - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals($function['body']['total'], 1); - $this->assertIsArray($function['body']['executions']); - $this->assertCount(1, $function['body']['executions']); - $this->assertEquals($function['body']['executions'][0]['$id'], $data['executionId']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ Query::limit(1)->toString(), ], ]); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(1, $executions['body']['executions']); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(1, $response['body']['executions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ Query::offset(1)->toString(), ], ]); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(0, $executions['body']['executions']); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(0, $response['body']['executions']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ Query::equal('trigger', ['http'])->toString(), ], ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(1, $response['body']['executions']); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(1, $executions['body']['executions']); /** * Test search queries */ - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($data['functionId'], [ 'search' => $data['executionId'], ]); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals(1, $executions['body']['total']); + $this->assertIsInt($executions['body']['total']); + $this->assertCount(1, $executions['body']['executions']); + $this->assertEquals($data['functionId'], $executions['body']['executions'][0]['functionId']); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['total']); - $this->assertIsInt($response['body']['total']); - $this->assertCount(1, $response['body']['executions']); - $this->assertEquals($data['functionId'], $response['body']['executions'][0]['functionId']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($data['functionId'], [ 'search' => $data['functionId'], ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['total']); - $this->assertIsInt($response['body']['total']); - $this->assertCount(1, $response['body']['executions']); - $this->assertEquals($data['executionId'], $response['body']['executions'][0]['$id']); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals(1, $executions['body']['total']); + $this->assertIsInt($executions['body']['total']); + $this->assertCount(1, $executions['body']['executions']); + $this->assertEquals($data['executionId'], $executions['body']['executions'][0]['$id']); return $data; } @@ -1193,13 +939,9 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($data['functionId'], [ // Testing default value, should be 'async' => false ]); - $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -1221,22 +963,14 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions/' . $data['executionId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals($function['headers']['status-code'], 200); - $this->assertEquals($function['body']['$id'], $data['executionId']); + $execution = $this->getExecution($data['functionId'], $data['executionId']); + $this->assertEquals($execution['headers']['status-code'], 200); + $this->assertEquals($execution['body']['$id'], $data['executionId']); /** * Test for FAILURE */ - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions/x', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $function = $this->getExecution($data['functionId'], 'x'); $this->assertEquals($function['headers']['status-code'], 404); return $data; @@ -1329,7 +1063,8 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $response1 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ + // Change the function specs + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -1344,22 +1079,19 @@ class FunctionsCustomServerTest extends Scope 'specification' => Specification::S_1VCPU_1GB, ]); - $this->assertEquals(200, $response1['headers']['status-code']); - $this->assertNotEmpty($response1['body']['$id']); - $this->assertEquals(Specification::S_1VCPU_1GB, $response1['body']['specification']); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + $this->assertEquals(Specification::S_1VCPU_1GB, $function['body']['specification']); - // Test Execution - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + // Verify the updated specs + $execution = $this->createExecution($data['functionId']); $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); $this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']); - $response2 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ + // Change the specs to 1vcpu 512mb + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -1374,25 +1106,21 @@ class FunctionsCustomServerTest extends Scope 'specification' => Specification::S_1VCPU_512MB, ]); - $this->assertEquals(200, $response2['headers']['status-code']); - $this->assertNotEmpty($response2['body']['$id']); - $this->assertEquals(Specification::S_1VCPU_512MB, $response2['body']['specification']); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + $this->assertEquals(Specification::S_1VCPU_512MB, $function['body']['specification']); - // Test Execution - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + // Verify the updated specs + $execution = $this->createExecution($data['functionId']); $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); $this->assertEquals(512, $output['APPWRITE_FUNCTION_MEMORY']); /** * Test for FAILURE */ - $response3 = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -1406,9 +1134,8 @@ class FunctionsCustomServerTest extends Scope 'entrypoint' => 'index.php', 'specification' => 's-2vcpu-512mb', // Invalid specification ]); - - $this->assertEquals(400, $response3['headers']['status-code']); - $this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $response3['body']['message']); + $this->assertEquals(400, $function['headers']['status-code']); + $this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $function['body']['message']); return $data; } @@ -1421,24 +1148,15 @@ class FunctionsCustomServerTest extends Scope /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/deployments/' . $data['deploymentId'], array_merge([ + $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/deployments/' . $data['deploymentId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(204, $deployment['headers']['status-code']); + $this->assertEmpty($deployment['body']); - $this->assertEquals(204, $function['headers']['status-code']); - $this->assertEmpty($function['body']); - - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $data['deploymentId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(404, $function['headers']['status-code']); - - /** - * Test for FAILURE - */ + $deployment = $this->getDeployment($data['functionId'], $data['deploymentId']); + $this->assertEquals(404, $deployment['headers']['status-code']); return $data; } @@ -1446,87 +1164,60 @@ class FunctionsCustomServerTest extends Scope /** * @depends testCreateDeployment */ - public function testDelete($data): array + public function testDeleteFunction($data): array { /** * Test for SUCCESS */ - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $function = $this->deleteFunction($data['functionId']); $this->assertEquals(204, $function['headers']['status-code']); $this->assertEmpty($function['body']); - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $function = $this->getFunction($data['functionId']); $this->assertEquals(404, $function['headers']['status-code']); - /** - * Test for FAILURE - */ - return $data; } - public function testTimeout() + public function testExecutionTimeout() { - $function = $this->createFunction([ + $functionId = $this->setupFunction([ 'name' => 'timeout-php-8.0', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'events' => [], - 'schedule' => '*/10 * * * *', // Execute every 10 seconds - 'timeout' => 15, + 'schedule' => '', + 'timeout' => 5, // Should timeout after 5 seconds + ]); + $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('php'), + 'activate' => true, ]); - $functionId = $function['body']['id']; - $this->assertEquals(201, $function['headers']['status-code']); - $deployment = $this->createDeployment($functionId, 'php'); - $deploymentId = $deployment['body']['$id']; - $this->assertDeploymentStatus($functionId, $deploymentId, 'ready'); - - $execution = $this->createExecution($functionId, ['async' => true]); + $execution = $this->createExecution($functionId, [ + 'async' => true + ]); $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(202, $execution['headers']['status-code']); + \sleep(5); // Wait for the function to timeout + $this->assertEventually(function () use ($functionId, $executionId) { $execution = $this->getExecution($functionId, $executionId); $this->assertEquals(200, $execution['headers']['status-code']); $this->assertEquals('failed', $execution['body']['status']); - $this->assertEquals('failed', $execution['body']['status']); $this->assertEquals(500, $execution['body']['responseStatusCode']); $this->assertGreaterThan(2, $execution['body']['duration']); $this->assertLessThan(20, $execution['body']['duration']); $this->assertEquals('', $execution['body']['responseBody']); $this->assertEquals('', $execution['body']['logs']); $this->assertStringContainsString('timed out', $execution['body']['errors']); - }, 20000, 500); + }, 5000, 250); - $this->assertEventually(function () use ($functionId) { - $executions = $this->listExecutions($functionId, [ - 'queries' => [ - Query::equal('trigger', ['schedule'])->toString(), - ], - ]); - - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertEquals(1, $executions['body']['total']); - - $lastExecution = $executions['body']['executions'][0]; - $this->assertEquals('schedule', $lastExecution['trigger']); - }, 20000, 500); - - $function = $this->deleteFunction($functionId); - $this->assertEquals(204, $function['headers']['status-code']); + $this->cleanupFunction($functionId); } - /** * * @return array @@ -1554,61 +1245,33 @@ class FunctionsCustomServerTest extends Scope */ public function testCreateCustomExecution(string $folder, string $name, string $entrypoint, string $runtimeName, string $runtimeVersion) { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test ' . $name, 'runtime' => $name, 'entrypoint' => $entrypoint, 'events' => [], - 'timeout' => $timeout, + 'timeout' => 15, ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $variable = $this->createVariable($functionId, [ 'key' => 'CUSTOM_VARIABLE', - 'value' => 'variable', + 'value' => 'variable' ]); - $this->assertEquals(201, $variable['headers']['status-code']); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deploymentId = $this->setupDeployment($functionId, [ 'entrypoint' => $entrypoint, - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction($folder), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', 'async' => false ]); - $executionId = $execution['body']['$id'] ?? ''; $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -1627,11 +1290,7 @@ class FunctionsCustomServerTest extends Scope $this->assertStringContainsString('Amazing Function Log', $execution['body']['logs']); $this->assertEmpty($execution['body']['errors']); - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - + $executions = $this->listExecutions($functionId); $this->assertEquals($executions['headers']['status-code'], 200); $this->assertEquals($executions['body']['total'], 1); $this->assertIsArray($executions['body']['executions']); @@ -1640,55 +1299,28 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($executions['body']['executions'][0]['trigger'], 'http'); $this->assertStringContainsString('Amazing Function Log', $executions['body']['executions'][0]['logs']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testCreateCustomExecutionBinaryResponse() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-binary-response/code.tar.gz"; - $this->packageCode('php-binary-response'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Binary executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, 'execute' => ['any'] ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-binary-response'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'x-appwrite-project' => $this->getProject()['$id'], - 'accept' => 'multipart/form-data', + 'accept' => 'multipart/form-data', // Accept binary response ], $this->getHeaders()), [ 'body' => null, ]); @@ -1708,7 +1340,7 @@ class FunctionsCustomServerTest extends Scope */ $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'x-appwrite-project' => $this->getProject()['$id'], - 'accept' => 'application/json', + 'accept' => 'application/json', // Accept JSON response ], $this->getHeaders()), [ 'body' => null, ]); @@ -1716,56 +1348,29 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(400, $execution['headers']['status-code']); $this->assertStringContainsString('Failed to parse response', $execution['body']['message']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testCreateCustomExecutionBinaryRequest() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-binary-request/code.tar.gz"; - $this->packageCode('php-binary-request'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Binary executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, 'execute' => ['any'] ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-binary-request'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $bytes = pack('C*', ...[0, 20, 255]); $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'multipart/form-data', + 'content-type' => 'multipart/form-data', // Send binary request 'x-appwrite-project' => $this->getProject()['$id'], 'accept' => 'application/json', ], $this->getHeaders()), [ @@ -1773,7 +1378,6 @@ class FunctionsCustomServerTest extends Scope ], false); $executionBody = json_decode($execution['body'], true); - $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals(\md5($bytes), $executionBody['responseBody']); $this->assertStringStartsWith('application/json', $execution['headers']['content-type']); @@ -1782,7 +1386,7 @@ class FunctionsCustomServerTest extends Scope * Test for FAILURE */ $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', + 'content-type' => 'application/json', // Send JSON headers 'x-appwrite-project' => $this->getProject()['$id'], 'accept' => 'application/json', ], $this->getHeaders()), [ @@ -1792,89 +1396,48 @@ class FunctionsCustomServerTest extends Scope $executionBody = json_decode($execution['body'], true); $this->assertNotEquals(\md5($bytes), $executionBody['responseBody']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testv2Function() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-v2/code.tar.gz"; - $this->packageCode('php-v2'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP V2', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'events' => [], - 'timeout' => $timeout, + 'timeout' => 15, ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $headers = [ + $variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', [ 'content-type' => 'application/json', 'origin' => 'http://localhost', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-mode' => 'admin', - ]; - - $variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', $headers, [ + ], [ 'functionId' => $functionId ]); - $this->assertEquals(204, $variable['headers']['status-code']); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-v2'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', 'async' => false ]); - $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); $this->assertEquals(true, $output['v2Woks']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testGetRuntimes() @@ -1902,14 +1465,7 @@ class FunctionsCustomServerTest extends Scope public function testEventTrigger() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-event/code.tar.gz"; - $this->packageCode('php-event'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Event executions', 'runtime' => 'php-8.0', @@ -1917,28 +1473,15 @@ class FunctionsCustomServerTest extends Scope 'events' => [ 'users.*.create', ], - 'timeout' => $timeout, + 'timeout' => 15, ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-event'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - // Create user to trigger event + // Create user as an event trigger $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1946,111 +1489,57 @@ class FunctionsCustomServerTest extends Scope 'userId' => 'unique()', 'name' => 'Event User' ]); - - $userId = $user['body']['$id']; + $userId = $user['body']['$id'] ?? ''; $this->assertEquals(201, $user['headers']['status-code']); - $execution = null; - $this->assertEventually(function () use ($functionId, &$execution) { - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]); + $this->assertEventually(function () use ($functionId, $userId) { + $executions = $this->listExecutions($functionId); + $lastExecution = $executions['body']['executions'][0]; + $this->assertEquals('completed', $lastExecution['status']); + $this->assertEquals(204, $lastExecution['responseStatusCode']); + $this->assertStringContainsString($userId, $lastExecution['logs']); + $this->assertStringContainsString('Event User', $lastExecution['logs']); + }, 10000, 500); - if (empty($executions['body']['executions'])) { - throw new \Exception('No executions found yet'); - } + $this->cleanupFunction($functionId); - $execution = $executions['body']['executions'][0]; - $this->assertEquals('completed', $execution['status']); - }, 30000, 1000); - - $this->assertEquals(204, $execution['responseStatusCode']); - $this->assertStringContainsString($userId, $execution['logs']); - $this->assertStringContainsString('Event User', $execution['logs']); - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + // Cleanup user + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], []); - - $this->assertEquals(204, $response['headers']['status-code']); - - // Cleanup : Delete user - $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEquals(204, $user['headers']['status-code']); } public function testScopes() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-scopes/code.tar.gz"; - $this->packageCode('php-scopes'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Scopes executions', 'commands' => 'composer update --no-interaction --ignore-platform-reqs --optimize-autoloader --prefer-dist --no-dev', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'scopes' => ['users.read'], - 'timeout' => $timeout, + 'timeout' => 15, ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $deploymentId = $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', 'commands' => 'sh setup.sh && composer install', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), - 'activate' => true + 'code' => $this->packageFunction('php-scopes'), + 'activate' => true, ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - - $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertStringContainsStringIgnoringCase("200 OK", $deployment['body']['buildLogs']); $this->assertStringContainsStringIgnoringCase('"total":', $deployment['body']['buildLogs']); $this->assertStringContainsStringIgnoringCase('"users":', $deployment['body']['buildLogs']); - $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $deployment['headers']['status-code']); - - // Wait a little for activation to finish - sleep(5); - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'async' => false + $execution = $this->createExecution($functionId, [ + 'async' => false, ]); $this->assertEquals(201, $execution['headers']['status-code']); @@ -2060,81 +1549,44 @@ class FunctionsCustomServerTest extends Scope $this->assertNotEmpty($execution['body']['responseBody']); $this->assertStringContainsString("total", $execution['body']['responseBody']); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'async' => true + $execution = $this->createExecution($functionId, [ + 'async' => true, ]); - + $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(202, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); - \sleep(10); + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $execution['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertGreaterThan(0, $execution['body']['duration']); + $this->assertNotEmpty($execution['body']['responseBody']); + $this->assertStringContainsString("total", $execution['body']['responseBody']); + }, 5000, 250); - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals('completed', $execution['body']['status']); - $this->assertEquals(200, $execution['body']['responseStatusCode']); - $this->assertGreaterThan(0, $execution['body']['duration']); - $this->assertNotEmpty($execution['body']['logs']); - $this->assertStringContainsString("total", $execution['body']['logs']); - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testCookieExecution() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; - $this->packageCode('php-cookie'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Cookie executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, ]); - - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-cookie'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; - - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => false, 'headers' => [ 'cookie' => $cookie @@ -2147,38 +1599,20 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($cookie, $execution['body']['responseBody']); $this->assertGreaterThan(0, $execution['body']['duration']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testFunctionsDomain() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; - $this->packageCode('php-cookie'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Cookie executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, 'execute' => ['any'] ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2196,20 +1630,12 @@ class FunctionsCustomServerTest extends Scope $domain = $rules['body']['rules'][0]['domain']; - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-cookie'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; $proxyClient = new Client(); @@ -2227,64 +1653,31 @@ class FunctionsCustomServerTest extends Scope // Await Aggregation sleep(App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); - $tries = 0; - while (true) { - try { - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ - 'range' => '24h' - ]); + $this->assertEventually(function () use ($functionId) { + $response = $this->getFunctionUsage($functionId, [ + 'range' => '24h' + ]); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(19, count($response['body'])); - $this->assertEquals('24h', $response['body']['range']); - $this->assertEquals(1, $response['body']['executionsTotal']); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(19, count($response['body'])); + $this->assertEquals('24h', $response['body']['range']); + $this->assertEquals(1, $response['body']['executionsTotal']); + }, 25000, 1000); - break; - } catch (ExpectationFailedException $th) { - if ($tries >= 5) { - throw $th; - } else { - $tries++; - sleep(5); - } - } - } - - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testFunctionsDomainBinaryResponse() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-binary-response/code.tar.gz"; - $this->packageCode('php-binary-response'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Binary executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, 'execute' => ['any'] ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2302,20 +1695,11 @@ class FunctionsCustomServerTest extends Scope $domain = $rules['body']['rules'][0]['domain']; - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), - 'activate' => true + 'code' => $this->packageFunction('php-binary-response'), ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -2329,38 +1713,20 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(10, $bytes['byte2']); $this->assertEquals(255, $bytes['byte3']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testFunctionsDomainBinaryRequest() { - $timeout = 15; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-binary-request/code.tar.gz"; - $this->packageCode('php-binary-request'); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Binary executions', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'timeout' => $timeout, + 'timeout' => 15, 'execute' => ['any'] ]); - $functionId = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2378,20 +1744,12 @@ class FunctionsCustomServerTest extends Scope $domain = $rules['body']['rules'][0]['domain']; - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('php-binary-request'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); - - $this->assertDeployment($function['body']['$id'], $deploymentId); - $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -2402,19 +1760,12 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(\md5($bytes), $response['body']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testCreateFunctionWithResponseFormatHeader() { - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.5.0', // add response format header @@ -2426,29 +1777,16 @@ class FunctionsCustomServerTest extends Scope 'timeout' => 15, ]); - $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(201, $function['headers']['status-code']); - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $response['body']['$id'], [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); + $functionId = $function['body']['$id'] ?? ''; - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } public function testFunctionLogging() { - // Preparations: Create Function - $folder = 'node'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function = $this->createFunction([ 'functionId' => ID::unique(), 'runtime' => 'node-18.0', 'name' => 'Logging Test', @@ -2461,38 +1799,22 @@ class FunctionsCustomServerTest extends Scope $this->assertFalse($function['body']['logging']); $this->assertNotEmpty($function['body']['$id']); - $functionId = $function['body']['$id']; + $functionId = $functionId = $function['body']['$id'] ?? ''; - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), 'activate' => true ]); - $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertNotEmpty($deployment['body']['$id']); - - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertDeployment($functionId, $deploymentId); - // Sync Executions test - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + $execution = $this->createExecution($functionId); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEmpty($execution['body']['logs']); $this->assertEmpty($execution['body']['errors']); // Async Executions test - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $execution = $this->createExecution($functionId, [ 'async' => true ]); @@ -2501,19 +1823,16 @@ class FunctionsCustomServerTest extends Scope $this->assertEmpty($execution['body']['errors']); $this->assertNotEmpty($execution['body']['$id']); - $executionId = $execution['body']['$id']; + $executionId = $execution['body']['$id'] ?? ''; - sleep(5); + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); - $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals('completed', $execution['body']['status']); - $this->assertEmpty($execution['body']['logs']); - $this->assertEmpty($execution['body']['errors']); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEmpty($execution['body']['logs']); + $this->assertEmpty($execution['body']['errors']); + }, 5000, 250); // Domain Executions test $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ @@ -2541,10 +1860,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $executions = $this->listExecutions($functionId, [ 'queries' => [ Query::limit(1)->toString(), Query::orderDesc('$id')->toString(), @@ -2557,10 +1873,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEmpty($executions['body']['executions'][0]['errors']); // Ensure executions count - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $executions = $this->listExecutions($functionId); $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(3, $executions['body']['executions']); @@ -2571,13 +1884,6 @@ class FunctionsCustomServerTest extends Scope $this->assertEmpty($execution['errors']); } - // Cleanup : Delete function - $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], []); - - $this->assertEquals(204, $response['headers']['status-code']); + $this->cleanupFunction($functionId); } } From 32657e22cc1e8d3ade1dca3fb52962277a4fe54f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:34:38 +0100 Subject: [PATCH 139/241] fix: conflicts --- tests/e2e/Services/Functions/FunctionsBase.php | 2 +- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 7cfa04b0ac..04039e3acc 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -146,7 +146,7 @@ trait FunctionsBase $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; if (!file_exists($code)) { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output, $this->output); } if (filesize($code) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index e383abe36f..1796871c26 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -513,7 +513,7 @@ class FunctionsCustomServerTest extends Scope $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output, $this->output); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); From 504711845aae015cbc487e000f5b944a33094fc9 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:46:05 +0100 Subject: [PATCH 140/241] fix: stdout --- tests/e2e/Services/Functions/FunctionsBase.php | 2 +- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 04039e3acc..d2bb19dea2 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -146,7 +146,7 @@ trait FunctionsBase $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; if (!file_exists($code)) { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output, $this->output); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); } if (filesize($code) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 1796871c26..7c4cd6d1ee 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -513,7 +513,7 @@ class FunctionsCustomServerTest extends Scope $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output, $this->output); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); From 65c0db32852f3eb41a560034b5fb64876d67e859 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:58:09 +0100 Subject: [PATCH 141/241] fix: package function --- tests/e2e/Services/Functions/FunctionsBase.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index d2bb19dea2..5ed87b4895 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -141,18 +141,20 @@ trait FunctionsBase return $executions; } - protected function packageFunction(string $folder = 'php'): CURLFile + protected function packageFunction(string $function = 'php'): CURLFile { - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; - if (!file_exists($code)) { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); + if (!file_exists($tarPath)) { + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); } - if (filesize($code) > 1024 * 1024 * 5) { + + if (filesize($tarPath) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); } - return new CURLFile($code, 'application/x-gzip', \basename($code)); + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); } protected function createDeployment( From cb0416be7bd294725c4c6c455c44c05d973f38c9 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:08:04 +0100 Subject: [PATCH 142/241] chore: fmt --- tests/e2e/General/UsageTest.php | 7 ++----- .../Functions/FunctionsCustomServerTest.php | 2 +- tests/e2e/Services/GraphQL/Base.php | 19 +++++++++++++++---- .../Services/GraphQL/FunctionsClientTest.php | 7 +------ .../Services/GraphQL/FunctionsServerTest.php | 7 +------ .../Realtime/RealtimeConsoleClientTest.php | 9 +-------- 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index d3623acffc..69460aa4cd 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -629,9 +629,6 @@ class UsageTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); - $code = realpath(__DIR__ . '/../../resources/functions') . "/php/code.tar.gz"; - $this->packageCode('php'); - $response = $this->client->call( Client::METHOD_POST, '/functions/' . $functionId . '/deployments', @@ -641,8 +638,8 @@ class UsageTest extends Scope ], $this->getHeaders()), [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), - 'activate' => true + 'code' => $this->packageFunction('php'), + 'activate' => true, ] ); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 7c4cd6d1ee..4ff6639864 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -775,7 +775,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn($deployment) => $deployment['$id'] === $deploymentId + fn ($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index b77f006bf8..449275f102 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2,6 +2,7 @@ namespace Tests\E2E\Services\GraphQL; +use CURLFile; use Utopia\CLI\Console; trait Base @@ -2493,11 +2494,21 @@ trait Base } // Function-related methods - protected string $stdout = ''; - protected string $stderr = ''; + protected string $output = ''; - protected function packageCode($folder): void + protected function packageFunction(string $function = 'php'): CURLFile { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout); + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; + + if (!file_exists($tarPath)) { + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); + } + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); } } diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php index 3f0ee1966a..e7e8421254 100644 --- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\GraphQL; -use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -83,10 +82,6 @@ class FunctionsClientTest extends Scope $projectId = $this->getProject()['$id']; $query = $this->getQuery(self::$CREATE_DEPLOYMENT); - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - $gqlPayload = [ 'operations' => \json_encode([ 'query' => $query, @@ -99,7 +94,7 @@ class FunctionsClientTest extends Scope 'map' => \json_encode([ 'code' => ["variables.code"] ]), - 'code' => new CURLFile($code, 'application/gzip', 'code.tar.gz'), + 'code' => $this->packageFunction('php') ]; $deployment = $this->client->call(Client::METHOD_POST, '/graphql', [ diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index 25a671fa1c..c3606244c4 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\GraphQL; -use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -82,10 +81,6 @@ class FunctionsServerTest extends Scope $projectId = $this->getProject()['$id']; $query = $this->getQuery(self::$CREATE_DEPLOYMENT); - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - $gqlPayload = [ 'operations' => \json_encode([ 'query' => $query, @@ -98,7 +93,7 @@ class FunctionsServerTest extends Scope 'map' => \json_encode([ 'code' => ["variables.code"] ]), - 'code' => new CURLFile($code, 'application/gzip', 'code.tar.gz'), + 'code' => $this->packageFunction('php'), ]; $deployment = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ diff --git a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php index 6ab2874f8e..b1777cdba9 100644 --- a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\Realtime; -use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -506,21 +505,15 @@ class RealtimeConsoleClientTest extends Scope * Test Create Deployment */ - $folder = 'php'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - $this->packageCode($folder); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertEquals(202, $deployment['headers']['status-code']); $response = json_decode($client->receive(), true); From 410a35deb510535de565915921afa6c09add1075 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:29:18 +0100 Subject: [PATCH 143/241] fix: spacing --- .../Functions/FunctionsCustomServerTest.php | 123 ++++++++++++++---- 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 4ff6639864..47e80efde9 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -38,7 +38,9 @@ class FunctionsCustomServerTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); + $functionId = $functionId = $function['body']['$id'] ?? ''; + $dateValidator = new DatetimeValidator(); $this->assertEquals(201, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); @@ -66,6 +68,7 @@ class FunctionsCustomServerTest extends Scope 'key' => 'funcKey3', 'value' => 'funcValue3', ]); + $this->assertEquals(201, $variable['headers']['status-code']); $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertEquals(201, $variable3['headers']['status-code']); @@ -87,6 +90,7 @@ class FunctionsCustomServerTest extends Scope $functions = $this->listFunctions([ 'search' => $data['functionId'] ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); @@ -97,6 +101,7 @@ class FunctionsCustomServerTest extends Scope Query::limit(1)->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); @@ -106,6 +111,7 @@ class FunctionsCustomServerTest extends Scope Query::offset(1)->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(0, $functions['body']['functions']); @@ -115,6 +121,7 @@ class FunctionsCustomServerTest extends Scope Query::equal('enabled', [true])->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); @@ -124,6 +131,7 @@ class FunctionsCustomServerTest extends Scope Query::equal('enabled', [false])->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(0, $functions['body']['functions']); @@ -131,6 +139,7 @@ class FunctionsCustomServerTest extends Scope $functions = $this->listFunctions([ 'search' => 'Test' ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); $this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']); @@ -139,6 +148,7 @@ class FunctionsCustomServerTest extends Scope $functions = $this->listExecutions([ 'search' => 'php-8.0' ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); $this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']); @@ -160,6 +170,7 @@ class FunctionsCustomServerTest extends Scope ]); $functions = $this->listFunctions(); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertEquals($functions['body']['total'], 2); $this->assertIsArray($functions['body']['functions']); @@ -172,6 +183,7 @@ class FunctionsCustomServerTest extends Scope Query::cursorAfter(new Document(['$id' => $functions['body']['functions'][0]['$id']]))->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); $this->assertEquals($functions['body']['functions'][0]['name'], 'Test 2'); @@ -181,6 +193,7 @@ class FunctionsCustomServerTest extends Scope Query::cursorBefore(new Document(['$id' => $functions['body']['functions'][1]['$id']]))->toString(), ], ]); + $this->assertEquals($functions['headers']['status-code'], 200); $this->assertCount(1, $functions['body']['functions']); $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); @@ -207,6 +220,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $function = $this->getFunction($data['functionId']); + $this->assertEquals($function['headers']['status-code'], 200); $this->assertEquals($function['body']['name'], 'Test'); @@ -214,6 +228,7 @@ class FunctionsCustomServerTest extends Scope * Test for FAILURE */ $function = $this->getFunction('x'); + $this->assertEquals($function['headers']['status-code'], 404); return $data; @@ -242,10 +257,11 @@ class FunctionsCustomServerTest extends Scope 'entrypoint' => 'index.php', ]); + $dateValidator = new DatetimeValidator(); + $this->assertEquals(200, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); $this->assertEquals('Test1', $function['body']['name']); - $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt'])); $this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt'])); $this->assertEquals('', $function['body']['deployment']); @@ -261,11 +277,8 @@ class FunctionsCustomServerTest extends Scope 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); - $this->assertEquals(201, $variable['headers']['status-code']); - /** - * Test for FAILURE - */ + $this->assertEquals(201, $variable['headers']['status-code']); return $data; } @@ -296,13 +309,15 @@ class FunctionsCustomServerTest extends Scope 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status']); $this->assertEquals('cli', $deployment['body']['type']); }, 500000, 1000); } @@ -338,23 +353,28 @@ class FunctionsCustomServerTest extends Scope 'templateVersion' => $starterTemplate['body']['providerVersion'], ] ); - $functionId = $functionId = $function['body']['$id'] ?? ''; + $this->assertEquals(201, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); + $functionId = $functionId = $function['body']['$id'] ?? ''; + $deployments = $this->listDeployments($functionId); + $this->assertEquals(200, $deployments['headers']['status-code']); $this->assertEquals(1, $deployments['body']['total']); $lastDeployment = $deployments['body']['deployments'][0]; + $this->assertNotEmpty($lastDeployment['$id']); $this->assertEquals(0, $lastDeployment['size']); $deploymentId = $lastDeployment['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status']); $this->assertEquals('cli', $deployment['body']['type']); }, 500000, 1000); @@ -366,6 +386,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($functionId, [ 'path' => '/ping', ]); + $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals("completed", $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -378,10 +399,12 @@ class FunctionsCustomServerTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], $this->getHeaders()), []); + $this->assertEquals(200, $users['headers']['status-code']); $this->assertIsInt($users['body']['total']); $totalUsers = $users['body']['total']; + $this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']); // Execute function again but async @@ -389,13 +412,16 @@ class FunctionsCustomServerTest extends Scope 'path' => '/ping', 'async' => true ]); - $executionId = $execution['body']['$id'] ?? ''; + $this->assertEquals(202, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); $this->assertEquals('waiting', $execution['body']['status']); + $executionId = $execution['body']['$id'] ?? ''; + $this->assertEventually(function () use ($functionId, $executionId, $totalUsers) { $execution = $this->getExecution($functionId, $executionId); + $this->assertEquals(200, $execution['headers']['status-code']); $this->assertEquals(200, $execution['body']['responseStatusCode']); $this->assertEquals('completed', $execution['body']['status']); @@ -405,7 +431,6 @@ class FunctionsCustomServerTest extends Scope }, 100000, 250); $function = $this->deleteFunction($functionId); - $this->assertEquals(204, $function['headers']['status-code']); } /** @@ -417,34 +442,39 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $functionId = $data['functionId']; + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), 'activate' => true ]); - $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); + $deploymentId = $deployment['body']['$id'] ?? ''; + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), 'activate' => false ]); + $deploymentIdInactive = $deployment['body']['$id']; + $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $this->assertEventually(function () use ($functionId, $deploymentId, $deploymentIdInactive) { $deployment = $this->getDeployment($functionId, $deploymentId); - $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status']); $deployment = $this->getDeployment($functionId, $deploymentIdInactive); - $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status']); }, 500000, 1000); $function = $this->getFunction($functionId); + $this->assertEquals(200, $function['headers']['status-code']); $this->assertEquals($deploymentId, $function['body']['deployment']); $this->assertNotEquals($deploymentIdInactive, $function['body']['deployment']); @@ -453,6 +483,7 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); + $this->assertEquals(204, $deployment['headers']['status-code']); return array_merge($data, ['deploymentId' => $deploymentId]); @@ -464,10 +495,12 @@ class FunctionsCustomServerTest extends Scope public function testCancelDeploymentBuild($data): void { $functionId = $data['functionId']; + $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), 'activate' => false ]); + $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); @@ -477,6 +510,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('building', $deployment['body']['status']); }, 100000, 250); @@ -486,6 +520,7 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(200, $cancel['headers']['status-code']); $this->assertEquals('canceled', $cancel['body']['status']); @@ -497,6 +532,7 @@ class FunctionsCustomServerTest extends Scope \sleep(30); $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); } @@ -554,8 +590,9 @@ class FunctionsCustomServerTest extends Scope $this->assertEventually(function () use ($functionId, $deploymentId, $deploymentSize) { $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('completed', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status']); $this->assertEquals($deploymentSize, $deployment['body']['size']); $this->assertGreaterThan(1024 * 1024 * 10, $deployment['body']['buildSize']); // ~7MB video file + 10MB sample file }, 500000, 1000); @@ -609,6 +646,7 @@ class FunctionsCustomServerTest extends Scope $deployments = $this->listDeployments($functionId, [ 'search' => $data['functionId'] ]); + $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(3, $deployments['body']['total']); $this->assertIsArray($deployments['body']['deployments']); @@ -621,6 +659,7 @@ class FunctionsCustomServerTest extends Scope Query::limit(1)->toString(), ], ]); + $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertCount(1, $deployments['body']['deployments']); @@ -655,7 +694,6 @@ class FunctionsCustomServerTest extends Scope 'search' => 'Test' ]); - $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(3, $deployments['body']['total']); $this->assertIsArray($deployments['body']['deployments']); @@ -753,6 +791,7 @@ class FunctionsCustomServerTest extends Scope Query::limit(1)->toString(), ] ); + $this->assertEquals(200, $deployments['headers']['status-code']); $this->assertGreaterThanOrEqual(1, $deployments['body']['total']); $this->assertNotEmpty($deployments['body']['deployments'][0]['$id']); @@ -775,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn ($deployment) => $deployment['$id'] === $deploymentId + fn($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); @@ -797,6 +836,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $deployment = $this->getDeployment($data['functionId'], $data['deploymentId']); + $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['buildTime']); $this->assertNotEmpty($deployment['body']['status']); @@ -808,6 +848,7 @@ class FunctionsCustomServerTest extends Scope * Test for FAILURE */ $deployment = $this->getDeployment($data['functionId'], 'x'); + $this->assertEquals($deployment['headers']['status-code'], 404); return $data; @@ -824,7 +865,6 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($data['functionId'], [ 'async' => false, ]); - $executionId = $execution['body']['$id'] ?? ''; $this->assertEquals(201, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); @@ -845,10 +885,13 @@ class FunctionsCustomServerTest extends Scope $this->assertNotEmpty($execution['body']['logs']); $this->assertLessThan(10, $execution['body']['duration']); + $executionId = $execution['body']['$id'] ?? ''; + $execution = $this->createExecution($data['functionId'], [ 'async' => false, 'path' => '/?code=400' ]); + $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(400, $execution['body']['responseStatusCode']); @@ -859,6 +902,7 @@ class FunctionsCustomServerTest extends Scope ], $this->getHeaders()), []); $execution = $this->createExecution($data['functionId']); + $this->assertEquals(204, $execution['headers']['status-code']); return array_merge($data, ['executionId' => $executionId]); @@ -873,6 +917,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $executions = $this->listExecutions($data['functionId']); + $this->assertEquals($executions['headers']['status-code'], 200); $this->assertEquals($executions['body']['total'], 1); $this->assertIsArray($executions['body']['executions']); @@ -884,6 +929,7 @@ class FunctionsCustomServerTest extends Scope Query::limit(1)->toString(), ], ]); + $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(1, $executions['body']['executions']); @@ -892,6 +938,7 @@ class FunctionsCustomServerTest extends Scope Query::offset(1)->toString(), ], ]); + $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(0, $executions['body']['executions']); @@ -900,6 +947,7 @@ class FunctionsCustomServerTest extends Scope Query::equal('trigger', ['http'])->toString(), ], ]); + $this->assertEquals(200, $executions['headers']['status-code']); $this->assertCount(1, $executions['body']['executions']); @@ -909,6 +957,7 @@ class FunctionsCustomServerTest extends Scope $executions = $this->listExecutions($data['functionId'], [ 'search' => $data['executionId'], ]); + $this->assertEquals(200, $executions['headers']['status-code']); $this->assertEquals(1, $executions['body']['total']); $this->assertIsInt($executions['body']['total']); @@ -918,6 +967,7 @@ class FunctionsCustomServerTest extends Scope $executions = $this->listExecutions($data['functionId'], [ 'search' => $data['functionId'], ]); + $this->assertEquals(200, $executions['headers']['status-code']); $this->assertEquals(1, $executions['body']['total']); $this->assertIsInt($executions['body']['total']); @@ -938,6 +988,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($data['functionId'], [ // Testing default value, should be 'async' => false ]); + $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); @@ -960,6 +1011,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $execution = $this->getExecution($data['functionId'], $data['executionId']); + $this->assertEquals($execution['headers']['status-code'], 200); $this->assertEquals($execution['body']['$id'], $data['executionId']); @@ -967,6 +1019,7 @@ class FunctionsCustomServerTest extends Scope * Test for FAILURE */ $function = $this->getExecution($data['functionId'], 'x'); + $this->assertEquals($function['headers']['status-code'], 404); return $data; @@ -1008,6 +1061,7 @@ class FunctionsCustomServerTest extends Scope ]); $executionId = $execution['body']['$id'] ?? ''; + $this->assertEquals(202, $execution['headers']['status-code']); $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, array_merge([ @@ -1039,6 +1093,7 @@ class FunctionsCustomServerTest extends Scope ]); $executionId = $execution['body']['$id'] ?? ''; + $this->assertEquals(202, $execution['headers']['status-code']); $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ @@ -1083,6 +1138,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($data['functionId']); $output = json_decode($execution['body']['responseBody'], true); + $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); $this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']); @@ -1110,6 +1166,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($data['functionId']); $output = json_decode($execution['body']['responseBody'], true); + $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); $this->assertEquals(512, $output['APPWRITE_FUNCTION_MEMORY']); @@ -1130,6 +1187,7 @@ class FunctionsCustomServerTest extends Scope 'entrypoint' => 'index.php', 'specification' => 's-2vcpu-512mb', // Invalid specification ]); + $this->assertEquals(400, $function['headers']['status-code']); $this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $function['body']['message']); @@ -1148,10 +1206,12 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(204, $deployment['headers']['status-code']); $this->assertEmpty($deployment['body']); $deployment = $this->getDeployment($data['functionId'], $data['deploymentId']); + $this->assertEquals(404, $deployment['headers']['status-code']); return $data; @@ -1166,10 +1226,12 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $function = $this->deleteFunction($data['functionId']); + $this->assertEquals(204, $function['headers']['status-code']); $this->assertEmpty($function['body']); $function = $this->getFunction($data['functionId']); + $this->assertEquals(404, $function['headers']['status-code']); return $data; @@ -1193,9 +1255,11 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($functionId, [ 'async' => true ]); - $executionId = $execution['body']['$id'] ?? ''; + $this->assertEquals(202, $execution['headers']['status-code']); + $executionId = $execution['body']['$id'] ?? ''; + \sleep(5); // Wait for the function to timeout $this->assertEventually(function () use ($functionId, $executionId) { @@ -1254,6 +1318,7 @@ class FunctionsCustomServerTest extends Scope 'key' => 'CUSTOM_VARIABLE', 'value' => 'variable' ]); + $this->assertEquals(201, $variable['headers']['status-code']); $deploymentId = $this->setupDeployment($functionId, [ @@ -1266,7 +1331,7 @@ class FunctionsCustomServerTest extends Scope 'body' => 'foobar', 'async' => false ]); - $executionId = $execution['body']['$id'] ?? ''; + $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); @@ -1286,7 +1351,10 @@ class FunctionsCustomServerTest extends Scope $this->assertStringContainsString('Amazing Function Log', $execution['body']['logs']); $this->assertEmpty($execution['body']['errors']); + $executionId = $execution['body']['$id'] ?? ''; + $executions = $this->listExecutions($functionId); + $this->assertEquals($executions['headers']['status-code'], 200); $this->assertEquals($executions['body']['total'], 1); $this->assertIsArray($executions['body']['executions']); @@ -1374,6 +1442,7 @@ class FunctionsCustomServerTest extends Scope ], false); $executionBody = json_decode($execution['body'], true); + $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals(\md5($bytes), $executionBody['responseBody']); $this->assertStringStartsWith('application/json', $execution['headers']['content-type']); @@ -1390,6 +1459,7 @@ class FunctionsCustomServerTest extends Scope ], false); $executionBody = json_decode($execution['body'], true); + $this->assertNotEquals(\md5($bytes), $executionBody['responseBody']); $this->cleanupFunction($functionId); @@ -1427,6 +1497,7 @@ class FunctionsCustomServerTest extends Scope 'body' => 'foobar', 'async' => false ]); + $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); @@ -1485,12 +1556,16 @@ class FunctionsCustomServerTest extends Scope 'userId' => 'unique()', 'name' => 'Event User' ]); - $userId = $user['body']['$id'] ?? ''; + $this->assertEquals(201, $user['headers']['status-code']); + $userId = $user['body']['$id'] ?? ''; + $this->assertEventually(function () use ($functionId, $userId) { $executions = $this->listExecutions($functionId); + $lastExecution = $executions['body']['executions'][0]; + $this->assertEquals('completed', $lastExecution['status']); $this->assertEquals(204, $lastExecution['responseStatusCode']); $this->assertStringContainsString($userId, $lastExecution['logs']); @@ -1548,10 +1623,12 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($functionId, [ 'async' => true, ]); - $executionId = $execution['body']['$id'] ?? ''; + $this->assertEquals(202, $execution['headers']['status-code']); $this->assertNotEmpty($execution['body']['$id']); + $executionId = $execution['body']['$id'] ?? ''; + $this->assertEventually(function () use ($functionId, $executionId) { $execution = $this->getExecution($functionId, $executionId); @@ -1646,7 +1723,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($cookie, $response['body']); - // Await Aggregation + // Wait for usage to be aggregated sleep(System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); $this->assertEventually(function () use ($functionId) { From 906ef59d95b221af625d508d54551e5c8d88fff7 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:36:19 +0100 Subject: [PATCH 144/241] chore: fmt --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 47e80efde9..82f80a8cd8 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -814,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn($deployment) => $deployment['$id'] === $deploymentId + fn ($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); From 19ea682a52e462418223654dc4e8f1fe3f8b2f1b Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:54:25 +0100 Subject: [PATCH 145/241] chore: timeouts --- .../Services/Functions/FunctionsCustomClientTest.php | 2 +- .../Services/Functions/FunctionsCustomServerTest.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index dd950ca655..53713cfd40 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -244,7 +244,7 @@ class FunctionsCustomClientTest extends Scope $execution = $this->getExecution($functionId, $executionId); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); - }, 10000, 250); + }, 10000, 500); return [ 'functionId' => $functionId diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 82f80a8cd8..b895e4c938 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -373,12 +373,14 @@ class FunctionsCustomServerTest extends Scope $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('ready', $deployment['body']['status']); $this->assertEquals('cli', $deployment['body']['type']); }, 500000, 1000); $function = $this->getFunction($functionId); + $this->assertEquals(200, $function['headers']['status-code']); $this->assertEquals($deploymentId, $function['body']['deployment']); @@ -428,7 +430,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEmpty($execution['body']['responseBody']); $this->assertEmpty($execution['body']['errors']); $this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']); - }, 100000, 250); + }, 10000, 500); $function = $this->deleteFunction($functionId); } @@ -1273,7 +1275,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals('', $execution['body']['responseBody']); $this->assertEquals('', $execution['body']['logs']); $this->assertStringContainsString('timed out', $execution['body']['errors']); - }, 5000, 250); + }, 10000, 500); $this->cleanupFunction($functionId); } @@ -1638,7 +1640,7 @@ class FunctionsCustomServerTest extends Scope $this->assertGreaterThan(0, $execution['body']['duration']); $this->assertNotEmpty($execution['body']['responseBody']); $this->assertStringContainsString("total", $execution['body']['responseBody']); - }, 5000, 250); + }, 10000, 500); $this->cleanupFunction($functionId); } @@ -1905,7 +1907,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals('completed', $execution['body']['status']); $this->assertEmpty($execution['body']['logs']); $this->assertEmpty($execution['body']['errors']); - }, 5000, 250); + }, 10000, 500); // Domain Executions test $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ From 92ce8d711d923a1b4398d5d1d6cc7ad847dfc8ff Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:10:04 +0100 Subject: [PATCH 146/241] feat: split scheduled tests --- .../Functions/FunctionsConsoleClientTest.php | 12 +- .../Functions/FunctionsCustomClientTest.php | 115 ---------- .../Functions/FunctionsCustomServerTest.php | 36 +--- .../Functions/FunctionsScheduleTest.php | 203 ++++++++++++++++++ 4 files changed, 216 insertions(+), 150 deletions(-) create mode 100644 tests/e2e/Services/Functions/FunctionsScheduleTest.php diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index cbfca09564..3a02cbcba2 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -30,9 +30,11 @@ class FunctionsConsoleClientTest extends Scope 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); - $functionId = $function['body']['$id']; + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + $function2 = $this->createFunction([ 'functionId' => ID::unique(), 'name' => 'Test Failure', @@ -40,6 +42,7 @@ class FunctionsConsoleClientTest extends Scope 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', ]); + $this->assertEquals(400, $function2['headers']['status-code']); return [ @@ -110,9 +113,11 @@ class FunctionsConsoleClientTest extends Scope 'value' => 'TESTINGVALUE' ] ); - $variableId = $variable['body']['$id']; + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + /** * Test for FAILURE */ @@ -124,6 +129,7 @@ class FunctionsConsoleClientTest extends Scope 'value' => 'ANOTHERTESTINGVALUE' ] ); + $this->assertEquals(409, $variable['headers']['status-code']); // Test for invalid key @@ -134,6 +140,7 @@ class FunctionsConsoleClientTest extends Scope 'value' => 'TESTINGVALUE' ] ); + $this->assertEquals(400, $variable['headers']['status-code']); // Test for invalid value @@ -144,6 +151,7 @@ class FunctionsConsoleClientTest extends Scope 'value' => str_repeat("#", 8193), ] ); + $this->assertEquals(400, $variable['headers']['status-code']); return array_merge( diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 53713cfd40..3447ee0547 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -48,7 +48,6 @@ class FunctionsCustomClientTest extends Scope 'users.*.create', 'users.*.delete', ], - 'schedule' => '* * * * *', // Execute every 60 seconds 'timeout' => 10, ]); $this->setupDeployment($functionId, [ @@ -72,124 +71,10 @@ class FunctionsCustomClientTest extends Scope ]); $this->assertEquals(202, $execution['headers']['status-code']); - // Wait for scheduled execution - \sleep(60); - - $this->assertEventually(function () use ($functionId) { - $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['key'], - ]); - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(2, $executions['body']['executions']); - - $asyncExecution = $executions['body']['executions'][1]; - $this->assertEquals('schedule', $asyncExecution['trigger']); - $this->assertEquals('completed', $asyncExecution['status']); - $this->assertEquals(200, $asyncExecution['responseStatusCode']); - $this->assertEquals('', $asyncExecution['responseBody']); - $this->assertNotEmpty($asyncExecution['logs']); - $this->assertNotEmpty($asyncExecution['errors']); - $this->assertGreaterThan(0, $asyncExecution['duration']); - }, 10000, 500); - $this->cleanupFunction($functionId); } - public function testCreateScheduledExecution(): void - { - /** - * Test for SUCCESS - */ - $functionId = $this->setupFunction([ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'execute' => [Role::user($this->getUser()['$id'])->toString()], - 'runtime' => 'php-8.0', - 'entrypoint' => 'index.php', - 'timeout' => 10, - 'logging' => true, - ]); - $this->setupDeployment($functionId, [ - 'entrypoint' => 'index.php', - 'code' => $this->packageFunction('php'), - 'activate' => true - ]); - // Schedule execution for the future - \date_default_timezone_set('UTC'); - $futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); // 2 minute in the future - $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); - - $execution = $this->createExecution($functionId, [ - 'async' => true, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), - 'path' => '/custom-path', - 'method' => 'PATCH', - 'body' => 'custom-body', - 'headers' => [ - 'x-custom-header' => 'custom-value' - ] - ]); - $executionId = $execution['body']['$id']; - - $this->assertEquals(202, $execution['headers']['status-code']); - $this->assertEquals('scheduled', $execution['body']['status']); - $this->assertEquals('PATCH', $execution['body']['requestMethod']); - $this->assertEquals('/custom-path', $execution['body']['requestPath']); - $this->assertCount(0, $execution['body']['requestHeaders']); - - \sleep(120); - - $this->assertEventually(function () use ($functionId, $executionId) { - $execution = $this->getExecution($functionId, $executionId); - - $this->assertEquals(200, $execution['headers']['status-code']); - $this->assertEquals(200, $execution['body']['responseStatusCode']); - $this->assertEquals('completed', $execution['body']['status']); - $this->assertEquals('/custom-path', $execution['body']['requestPath']); - $this->assertEquals('PATCH', $execution['body']['requestMethod']); - $this->assertStringContainsString('body-is-custom-body', $execution['body']['logs']); - $this->assertStringContainsString('custom-header-is-custom-value', $execution['body']['logs']); - $this->assertStringContainsString('method-is-patch', $execution['body']['logs']); - $this->assertStringContainsString('path-is-/custom-path', $execution['body']['logs']); - $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); - $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); - $this->assertGreaterThan(0, $execution['body']['duration']); - }, 10000, 500); - - /* Test for FAILURE */ - // Schedule synchronous execution - $execution = $this->createExecution($functionId, [ - 'async' => false, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), - ]); - $this->assertEquals(400, $execution['headers']['status-code']); - - // Execution with seconds precision - $execution = $this->createExecution($functionId, [ - 'async' => true, - 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02"))->format(\DateTime::ATOM) - ]); - $this->assertEquals(400, $execution['headers']['status-code']); - - // Execution with milliseconds precision - $execution = $this->createExecution($functionId, [ - 'async' => true, - 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02.255"))->format(\DateTime::ATOM) - ]); - $this->assertEquals(400, $execution['headers']['status-code']); - - // Execution too soon - $execution = $this->createExecution($functionId, [ - 'async' => true, - 'scheduledAt' => (new \DateTime())->add(new \DateInterval('PT1S'))->format(\DateTime::ATOM) - ]); - $this->assertEquals(400, $execution['headers']['status-code']); - - $this->cleanupFunction($functionId, $executionId); - } public function testCreateCustomExecution(): array { diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index b895e4c938..71dce2258c 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -35,7 +35,6 @@ class FunctionsCustomServerTest extends Scope 'buckets.*.create', 'buckets.*.delete', ], - 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); @@ -53,7 +52,7 @@ class FunctionsCustomServerTest extends Scope 'buckets.*.create', 'buckets.*.delete', ], $function['body']['events']); - $this->assertEquals('0 0 1 1 *', $function['body']['schedule']); + $this->assertEmpty($function['body']['schedule']); $this->assertEquals(10, $function['body']['timeout']); $variable = $this->createVariable($functionId, [ @@ -156,7 +155,7 @@ class FunctionsCustomServerTest extends Scope /** * Test pagination */ - $functionId = $this->setupFunction([ + $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test 2', 'runtime' => 'php-8.0', @@ -165,7 +164,6 @@ class FunctionsCustomServerTest extends Scope 'buckets.*.create', 'buckets.*.delete', ], - 'schedule' => '0 0 1 1 *', 'timeout' => 10, ]); @@ -816,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn ($deployment) => $deployment['$id'] === $deploymentId + fn($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); @@ -1078,35 +1076,7 @@ class FunctionsCustomServerTest extends Scope return $data; } - /** - * @depends testGetExecution - */ - public function testDeleteScheduledExecution($data): array - { - $futureTime = (new \DateTime())->add(new \DateInterval('PT10H')); - $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'async' => true, - 'scheduledAt' => $futureTime->format('Y-m-d H:i:s'), - ]); - - $executionId = $execution['body']['$id'] ?? ''; - - $this->assertEquals(202, $execution['headers']['status-code']); - - $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]); - - $this->assertEquals(204, $execution['headers']['status-code']); - - return $data; - } /** * @depends testGetExecution diff --git a/tests/e2e/Services/Functions/FunctionsScheduleTest.php b/tests/e2e/Services/Functions/FunctionsScheduleTest.php new file mode 100644 index 0000000000..bab9a2a684 --- /dev/null +++ b/tests/e2e/Services/Functions/FunctionsScheduleTest.php @@ -0,0 +1,203 @@ +setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [ + 'users.*.create', + 'users.*.delete', + ], + 'schedule' => '* * * * *', // Execute every 60 seconds + 'timeout' => 10, + ]); + + $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + + // Wait for scheduled execution + \sleep(60); + + $this->assertEventually(function () use ($functionId) { + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['key'], + ]); + + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(1, $executions['body']['executions']); + + $asyncExecution = $executions['body']['executions'][0]; + + $this->assertEquals('schedule', $asyncExecution['trigger']); + $this->assertEquals('completed', $asyncExecution['status']); + $this->assertEquals(200, $asyncExecution['responseStatusCode']); + $this->assertEquals('', $asyncExecution['responseBody']); + $this->assertNotEmpty($asyncExecution['logs']); + $this->assertNotEmpty($asyncExecution['errors']); + $this->assertGreaterThan(0, $asyncExecution['duration']); + }, 10000, 500); + + $this->cleanupFunction($functionId); + } + + public function testCreateScheduledAtExecution(): void + { + /** + * Test for SUCCESS + */ + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 10, + 'logging' => true, + ]); + $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + + // Schedule execution for the future + \date_default_timezone_set('UTC'); + $futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); // 2 minute in the future + $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); + + $execution = $this->createExecution($functionId, [ + 'async' => true, + 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + 'path' => '/custom-path', + 'method' => 'PATCH', + 'body' => 'custom-body', + 'headers' => [ + 'x-custom-header' => 'custom-value' + ] + ]); + $executionId = $execution['body']['$id']; + + $this->assertEquals(202, $execution['headers']['status-code']); + $this->assertEquals('scheduled', $execution['body']['status']); + $this->assertEquals('PATCH', $execution['body']['requestMethod']); + $this->assertEquals('/custom-path', $execution['body']['requestPath']); + $this->assertCount(0, $execution['body']['requestHeaders']); + + \sleep(120); + + $this->assertEventually(function () use ($functionId, $executionId) { + $execution = $this->getExecution($functionId, $executionId); + + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals('/custom-path', $execution['body']['requestPath']); + $this->assertEquals('PATCH', $execution['body']['requestMethod']); + $this->assertStringContainsString('body-is-custom-body', $execution['body']['logs']); + $this->assertStringContainsString('custom-header-is-custom-value', $execution['body']['logs']); + $this->assertStringContainsString('method-is-patch', $execution['body']['logs']); + $this->assertStringContainsString('path-is-/custom-path', $execution['body']['logs']); + $this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']); + $this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']); + $this->assertGreaterThan(0, $execution['body']['duration']); + }, 10000, 500); + + /* Test for FAILURE */ + // Schedule synchronous execution + $execution = $this->createExecution($functionId, [ + 'async' => false, + 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + ]); + $this->assertEquals(400, $execution['headers']['status-code']); + + // Execution with seconds precision + $execution = $this->createExecution($functionId, [ + 'async' => true, + 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02"))->format(\DateTime::ATOM) + ]); + $this->assertEquals(400, $execution['headers']['status-code']); + + // Execution with milliseconds precision + $execution = $this->createExecution($functionId, [ + 'async' => true, + 'scheduledAt' => (new \DateTime("2100-12-08 16:12:02.255"))->format(\DateTime::ATOM) + ]); + $this->assertEquals(400, $execution['headers']['status-code']); + + // Execution too soon + $execution = $this->createExecution($functionId, [ + 'async' => true, + 'scheduledAt' => (new \DateTime())->add(new \DateInterval('PT1S'))->format(\DateTime::ATOM) + ]); + $this->assertEquals(400, $execution['headers']['status-code']); + + $this->cleanupFunction($functionId, $executionId); + } + + public function testDeleteScheduledExecution() + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 10, + 'logging' => true, + ]); + + $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + + $futureTime = (new \DateTime())->add(new \DateInterval('PT10H')); + $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); + + $execution = $this->createExecution($functionId, [ + 'async' => true, + 'scheduledAt' => $futureTime->format('Y-m-d H:i:s'), + ]); + + $this->assertEquals(202, $execution['headers']['status-code']); + + $executionId = $execution['body']['$id'] ?? ''; + + $execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $execution['headers']['status-code']); + + $this->cleanupFunction($functionId); + } +} From f04e47b3d455b99ed6d36d04baa6c61143e05313 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:11:44 +0100 Subject: [PATCH 147/241] chore: fmt --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 71dce2258c..5db051da2b 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -814,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn($deployment) => $deployment['$id'] === $deploymentId + fn ($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); From b621ec2e8bdf5f7bda51da1e63e4708dd3ff1f9d Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:50:30 +0530 Subject: [PATCH 148/241] test for response and request filters --- .../Functions/FunctionsCustomServerTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index dccbf2e544..4dabec67f0 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2685,6 +2685,27 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); + // get function with 1.5.0 response format + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.5.0', // add response format header + ], $this->getHeaders())); + + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertArrayNotHasKey('scopes', $function['body']); + $this->assertArrayNotHasKey('specification', $function['body']); + + // get function without response format header + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertArrayHasKey('scopes', $function['body']); + $this->assertArrayHasKey('specification', $function['body']); + // Cleanup : Delete function $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $response['body']['$id'], [ 'content-type' => 'application/json', From 321227db15a5fc9dbbba5ecc667b6dbcd7cbd05c Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:15:05 +0100 Subject: [PATCH 149/241] fix: seperate tests --- .github/workflows/tests.yml | 3 ++- tests/e2e/Services/Functions/FunctionsBase.php | 18 ++++++++---------- .../Functions/FunctionsCustomServerTest.php | 11 ++++++----- .../FunctionsScheduleTest.php | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) rename tests/e2e/Services/{Functions => FunctionsSchedule}/FunctionsScheduleTest.php (98%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 633bd46ea4..5b82cba594 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,6 +113,7 @@ jobs: Console, Databases, Functions, + FunctionsSchedule, GraphQL, Health, Locale, @@ -141,7 +142,7 @@ jobs: run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose up -d - sleep 25 + sleep 10 - name: Run ${{matrix.service}} Tests run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 5ed87b4895..8b19c65705 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -101,7 +101,7 @@ trait FunctionsBase return $deployment; } - protected function getExecution($functionId, $executionId) + protected function getExecution(string $functionId, $executionId): mixed { $execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([ 'content-type' => 'application/json', @@ -111,7 +111,7 @@ trait FunctionsBase return $execution; } - protected function listFunctions($params = []) + protected function listFunctions(mixed $params = []): mixed { $functions = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ 'content-type' => 'application/json', @@ -121,7 +121,7 @@ trait FunctionsBase return $functions; } - protected function listDeployments($functionId, $params = []) + protected function listDeployments(string $functionId, $params = []): mixed { $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'application/json', @@ -131,7 +131,7 @@ trait FunctionsBase return $deployments; } - protected function listExecutions($functionId, $params = []) + protected function listExecutions(string $functionId, mixed $params = []): mixed { $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', @@ -157,10 +157,8 @@ trait FunctionsBase return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); } - protected function createDeployment( - string $functionId, - mixed $params - ) { + protected function createDeployment(string $functionId, mixed $params = []): mixed + { $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], @@ -189,7 +187,7 @@ trait FunctionsBase return $template; } - protected function createExecution(string $functionId, mixed $params = []) + protected function createExecution(string $functionId, mixed $params = []): mixed { $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', @@ -199,7 +197,7 @@ trait FunctionsBase return $execution; } - protected function deleteFunction($functionId) + protected function deleteFunction(string $functionId): mixed { $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 5db051da2b..b57cd3d0b8 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -144,7 +144,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']); // Test search runtime - $functions = $this->listExecutions([ + $functions = $this->listFunctions([ 'search' => 'php-8.0' ]); @@ -490,7 +490,7 @@ class FunctionsCustomServerTest extends Scope } /** - * @depends testUpdate + * @depends testUpdateFunction */ public function testCancelDeploymentBuild($data): void { @@ -538,7 +538,7 @@ class FunctionsCustomServerTest extends Scope } /** - * @depends testUpdate + * @depends testUpdateFunction */ public function testCreateDeploymentLarge($data): array { @@ -814,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn ($deployment) => $deployment['$id'] === $deploymentId + fn($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); @@ -1273,7 +1273,7 @@ class FunctionsCustomServerTest extends Scope * @param string $entrypoint * * @dataProvider provideCustomExecutions - * @depends testTimeout + * @depends testExecutionTimeout */ public function testCreateCustomExecution(string $folder, string $name, string $entrypoint, string $runtimeName, string $runtimeVersion) { @@ -1743,6 +1743,7 @@ class FunctionsCustomServerTest extends Scope $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', 'code' => $this->packageFunction('php-binary-response'), + 'activate' => true ]); $proxyClient = new Client(); diff --git a/tests/e2e/Services/Functions/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php similarity index 98% rename from tests/e2e/Services/Functions/FunctionsScheduleTest.php rename to tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index bab9a2a684..f39efb42b5 100644 --- a/tests/e2e/Services/Functions/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -6,14 +6,14 @@ use Appwrite\ID; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; -use Tests\E2E\Scopes\SideServer; +use Tests\E2E\Scopes\SideClient; use Utopia\Database\Helpers\Role; class FunctionsScheduleTest extends Scope { use FunctionsBase; use ProjectCustom; - use SideServer; + use SideClient; public function testCreateScheduledExecution() { @@ -47,7 +47,7 @@ class FunctionsScheduleTest extends Scope $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['key'], + 'x-appwrite-key' => $this->getProject()['apiKey'], ]); $this->assertEquals(200, $executions['headers']['status-code']); From 72ddef855ed6452c7ffd9dd5d82ec43908ef01f0 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:16:18 +0100 Subject: [PATCH 150/241] chore: fmt --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index b57cd3d0b8..e9297a109f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -814,7 +814,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn($deployment) => $deployment['$id'] === $deploymentId + fn ($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); From e4ad2ed566e349c1bfa849074bcbebeafaf8446f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:33:26 +0100 Subject: [PATCH 151/241] fix: realtime tests --- .../FunctionsScheduleTest.php | 33 ++++++++++++------- tests/e2e/Services/Realtime/RealtimeBase.php | 21 ++++++++++++ .../Realtime/RealtimeCustomClientTest.php | 9 +---- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index f39efb42b5..7e36662e9e 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -6,14 +6,14 @@ use Appwrite\ID; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; -use Tests\E2E\Scopes\SideClient; +use Tests\E2E\Scopes\SideServer; use Utopia\Database\Helpers\Role; class FunctionsScheduleTest extends Scope { use FunctionsBase; use ProjectCustom; - use SideClient; + use SideServer; public function testCreateScheduledExecution() { @@ -92,16 +92,27 @@ class FunctionsScheduleTest extends Scope $futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); // 2 minute in the future $futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0); - $execution = $this->createExecution($functionId, [ - 'async' => true, - 'scheduledAt' => $futureTime->format(\DateTime::ATOM), - 'path' => '/custom-path', - 'method' => 'PATCH', - 'body' => 'custom-body', - 'headers' => [ - 'x-custom-header' => 'custom-value' + + $execution = $this->client->call( + Client::METHOD_POST, + '/functions/' . $functionId . '/executions', + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $this->getUser()['session'], + ], + [ + 'async' => true, + 'scheduledAt' => $futureTime->format(\DateTime::ATOM), + 'path' => '/custom-path', + 'method' => 'PATCH', + 'body' => 'custom-body', + 'headers' => [ + 'x-custom-header' => 'custom-value' + ] ] - ]); + ); $executionId = $execution['body']['$id']; $this->assertEquals(202, $execution['headers']['status-code']); diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 30c411ba93..f0cd0e7c3b 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -2,6 +2,8 @@ namespace Tests\E2E\Services\Realtime; +use CURLFile; +use Utopia\CLI\Console; use WebSocket\Client as WebSocketClient; use WebSocket\ConnectionException; @@ -71,4 +73,23 @@ trait RealtimeBase $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); } + + // Function-related methods + protected string $output = ''; + + protected function packageFunction(string $function = 'php'): CURLFile + { + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; + + if (!file_exists($tarPath)) { + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); + } + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 86aaee2ab0..4c23bb2eb9 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -7,7 +7,6 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; -use Utopia\CLI\Console; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -1271,19 +1270,13 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals($function['headers']['status-code'], 201); $this->assertNotEmpty($function['body']['$id']); - $folder = 'timeout'; - $output = ''; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'entrypoint' => 'index.php', - 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'code' => $this->packageFunction('timeout'), 'activate' => true ]); From f24863349e133f51712aae84ba686ae86eaf3437 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:41:34 +0100 Subject: [PATCH 152/241] fixes --- .../Functions/FunctionsCustomServerTest.php | 24 ++++++++++--------- tests/e2e/Services/Realtime/RealtimeBase.php | 21 ---------------- .../Realtime/RealtimeCustomClientTest.php | 2 ++ 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index e9297a109f..d95d611f13 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -176,25 +176,25 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); $this->assertEquals($functions['body']['functions'][1]['name'], 'Test 2'); - $functions = $this->listFunctions([ + $functions1 = $this->listFunctions([ 'queries' => [ Query::cursorAfter(new Document(['$id' => $functions['body']['functions'][0]['$id']]))->toString(), ], ]); - $this->assertEquals($functions['headers']['status-code'], 200); - $this->assertCount(1, $functions['body']['functions']); - $this->assertEquals($functions['body']['functions'][0]['name'], 'Test 2'); + $this->assertEquals($functions1['headers']['status-code'], 200); + $this->assertCount(1, $functions1['body']['functions']); + $this->assertEquals($functions1['body']['functions'][0]['name'], 'Test 2'); - $functions = $this->listFunctions([ + $functions2 = $this->listFunctions([ 'queries' => [ Query::cursorBefore(new Document(['$id' => $functions['body']['functions'][1]['$id']]))->toString(), ], ]); - $this->assertEquals($functions['headers']['status-code'], 200); - $this->assertCount(1, $functions['body']['functions']); - $this->assertEquals($functions['body']['functions'][0]['name'], 'Test'); + $this->assertEquals($functions2['headers']['status-code'], 200); + $this->assertCount(1, $functions2['body']['functions']); + $this->assertEquals($functions2['body']['functions'][0]['name'], 'Test'); /** * Test for FAILURE @@ -285,7 +285,7 @@ class FunctionsCustomServerTest extends Scope { $functionId = $this->setupFunction([ 'functionId' => ID::unique(), - 'name' => 'test-cli-deployment', + 'name' => 'Test', 'execute' => [Role::user($this->getUser()['$id'])->toString()], 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', @@ -297,12 +297,12 @@ class FunctionsCustomServerTest extends Scope 'timeout' => 10, ]); - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], 'x-sdk-language' => 'cli', - ], $this->getHeaders()), [ + ], [ 'entrypoint' => 'index.php', 'code' => $this->packageFunction('php'), 'activate' => true @@ -314,6 +314,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEventually(function () use ($functionId, $deploymentId) { $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('ready', $deployment['body']['status']); $this->assertEquals('cli', $deployment['body']['type']); @@ -1212,6 +1213,7 @@ class FunctionsCustomServerTest extends Scope public function testExecutionTimeout() { $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), 'name' => 'timeout-php-8.0', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index f0cd0e7c3b..30c411ba93 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -2,8 +2,6 @@ namespace Tests\E2E\Services\Realtime; -use CURLFile; -use Utopia\CLI\Console; use WebSocket\Client as WebSocketClient; use WebSocket\ConnectionException; @@ -73,23 +71,4 @@ trait RealtimeBase $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); } - - // Function-related methods - protected string $output = ''; - - protected function packageFunction(string $function = 'php'): CURLFile - { - $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; - $tarPath = "$folderPath/code.tar.gz"; - - if (!file_exists($tarPath)) { - Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); - } - - if (filesize($tarPath) > 1024 * 1024 * 5) { - throw new \Exception('Code package is too large. Use the chunked upload method instead.'); - } - - return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); - } } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 4c23bb2eb9..260d5da3bf 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -7,6 +7,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; +use Tests\E2E\Services\Functions\FunctionsBase; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -14,6 +15,7 @@ use WebSocket\ConnectionException; class RealtimeCustomClientTest extends Scope { + use FunctionsBase; use RealtimeBase; use ProjectCustom; use SideClient; From 1509aa544de051c61856fe4cc42d64fcc3ab2d3c Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:43:41 +0100 Subject: [PATCH 153/241] debug: output fail deployment --- tests/e2e/Services/Functions/FunctionsBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 8b19c65705..529846c623 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -44,7 +44,7 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ])); - $this->assertEquals('ready', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 50000, 500); return $deploymentId; From 39490e981988ce24ead726a47e571ad9a6053c7f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:06:58 +0100 Subject: [PATCH 154/241] chore: bump tests --- .github/workflows/tests.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b82cba594..4bc7b18d23 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,22 +16,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build Appwrite - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . push: false tags: ${{ env.IMAGE }} load: true - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=appwrite + cache-to: type=gha,mode=max,scope=appwrite outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar build-args: | DEBUG=false @@ -39,9 +39,11 @@ jobs: VERSION=dev - name: Cache Docker Image - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} + restore-keys: | + appwrite-dev- path: /tmp/${{ env.IMAGE }}.tar unit_test: @@ -51,10 +53,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -81,10 +83,10 @@ jobs: needs: setup steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -129,10 +131,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -142,7 +144,7 @@ jobs: run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose up -d - sleep 10 + sleep 25 - name: Run ${{matrix.service}} Tests run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug @@ -150,15 +152,15 @@ jobs: - name: Run ${{matrix.service}} Shared Tables Tests run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug - benchamrking: + benchmarking: name: Benchmark runs-on: ubuntu-latest needs: setup steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar From 8e13aafbc854ff8cba1b1b62c2683af97ca42bc6 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:29:49 +0100 Subject: [PATCH 155/241] fixes --- .../Functions/FunctionsCustomServerTest.php | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index d95d611f13..66f77a1df8 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -454,30 +454,34 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $this->assertEquals('index.php', $deployment['body']['entrypoint']); - $deploymentId = $deployment['body']['$id'] ?? ''; + $deploymentIdActive = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($functionId, $deploymentIdActive) { + $deployment = $this->getDeployment($functionId, $deploymentIdActive); + + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), 'activate' => false ]); - $deploymentIdInactive = $deployment['body']['$id']; - $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $this->assertEventually(function () use ($functionId, $deploymentId, $deploymentIdInactive) { - $deployment = $this->getDeployment($functionId, $deploymentId); - $this->assertEquals('ready', $deployment['body']['status']); + $deploymentIdInactive = $deployment['body']['$id'] ?? ''; + $this->assertEventually(function () use ($functionId, $deploymentIdInactive) { $deployment = $this->getDeployment($functionId, $deploymentIdInactive); + $this->assertEquals('ready', $deployment['body']['status']); - }, 500000, 1000); + }, 50000, 500); $function = $this->getFunction($functionId); $this->assertEquals(200, $function['headers']['status-code']); - $this->assertEquals($deploymentId, $function['body']['deployment']); + $this->assertEquals($deploymentIdActive, $function['body']['deployment']); $this->assertNotEquals($deploymentIdInactive, $function['body']['deployment']); $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/deployments/' . $deploymentIdInactive, array_merge([ @@ -487,7 +491,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $deployment['headers']['status-code']); - return array_merge($data, ['deploymentId' => $deploymentId]); + return array_merge($data, ['deploymentId' => $deploymentIdActive]); } /** @@ -1222,7 +1226,7 @@ class FunctionsCustomServerTest extends Scope 'timeout' => 5, // Should timeout after 5 seconds ]); $this->setupDeployment($functionId, [ - 'code' => $this->packageFunction('php'), + 'code' => $this->packageFunction('timeout'), 'activate' => true, ]); @@ -1562,7 +1566,7 @@ class FunctionsCustomServerTest extends Scope $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test PHP Scopes executions', - 'commands' => 'composer update --no-interaction --ignore-platform-reqs --optimize-autoloader --prefer-dist --no-dev', + 'commands' => 'sh setup.sh && composer install', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'scopes' => ['users.read'], @@ -1571,7 +1575,6 @@ class FunctionsCustomServerTest extends Scope $deploymentId = $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', - 'commands' => 'sh setup.sh && composer install', 'code' => $this->packageFunction('php-scopes'), 'activate' => true, ]); From 41490602f77b59677d64c9e6b67af1ee1b52d774 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:38:43 +0100 Subject: [PATCH 156/241] fix: template --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 1 - tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 66f77a1df8..7021030331 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -375,7 +375,6 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('ready', $deployment['body']['status']); - $this->assertEquals('cli', $deployment['body']['type']); }, 500000, 1000); $function = $this->getFunction($functionId); diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index 7e36662e9e..bee15693f1 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -62,7 +62,7 @@ class FunctionsScheduleTest extends Scope $this->assertNotEmpty($asyncExecution['logs']); $this->assertNotEmpty($asyncExecution['errors']); $this->assertGreaterThan(0, $asyncExecution['duration']); - }, 10000, 500); + }, 15000, 500); $this->cleanupFunction($functionId); } From 2d102a122c4c1f8c60110770a6285bbe445e33ab Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:42:08 +0100 Subject: [PATCH 157/241] fix: tests --- .../Functions/FunctionsCustomServerTest.php | 47 +++++-------------- .../FunctionsScheduleTest.php | 2 +- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 7021030331..fec815c2ef 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -463,7 +463,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => false + 'activate' => 'false' ]); $this->assertEquals(202, $deployment['headers']['status-code']); @@ -502,7 +502,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => false + 'activate' => 'false' ]); $deploymentId = $deployment['body']['$id'] ?? ''; @@ -646,18 +646,6 @@ class FunctionsCustomServerTest extends Scope $this->assertArrayHasKey('size', $deployments['body']['deployments'][0]); $this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]); - // Test search id - $deployments = $this->listDeployments($functionId, [ - 'search' => $data['functionId'] - ]); - - $this->assertEquals($deployments['headers']['status-code'], 200); - $this->assertEquals(3, $deployments['body']['total']); - $this->assertIsArray($deployments['body']['deployments']); - $this->assertCount(3, $deployments['body']['deployments']); - $this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']); - - // Test pagination limit $deployments = $this->listDeployments($functionId, [ 'queries' => [ Query::limit(1)->toString(), @@ -694,16 +682,6 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertCount(0, $deployments['body']['deployments']); - $deployments = $this->listDeployments($functionId, [ - 'search' => 'Test' - ]); - - $this->assertEquals($deployments['headers']['status-code'], 200); - $this->assertEquals(3, $deployments['body']['total']); - $this->assertIsArray($deployments['body']['deployments']); - $this->assertCount(3, $deployments['body']['deployments']); - $this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']); - $deployments = $this->listDeployments($functionId, [ 'search' => 'php-8.0' ]); @@ -818,7 +796,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn ($deployment) => $deployment['$id'] === $deploymentId + fn($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); @@ -905,8 +883,6 @@ class FunctionsCustomServerTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); - $execution = $this->createExecution($data['functionId']); - $this->assertEquals(204, $execution['headers']['status-code']); return array_merge($data, ['executionId' => $executionId]); @@ -922,11 +898,12 @@ class FunctionsCustomServerTest extends Scope */ $executions = $this->listExecutions($data['functionId']); - $this->assertEquals($executions['headers']['status-code'], 200); - $this->assertEquals($executions['body']['total'], 1); + fwrite(STDERR, print_r(json_encode($executions, JSON_PRETTY_PRINT), true)); + + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertEquals(1, $executions['body']['total']); $this->assertIsArray($executions['body']['executions']); $this->assertCount(1, $executions['body']['executions']); - $this->assertEquals($executions['body']['executions'][0]['$id'], $data['executionId']); $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ @@ -939,12 +916,12 @@ class FunctionsCustomServerTest extends Scope $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ - Query::offset(1)->toString(), + Query::offset(0)->toString(), ], ]); $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(0, $executions['body']['executions']); + $this->assertCount(1, $executions['body']['executions']); $executions = $this->listExecutions($data['functionId'], [ 'queries' => [ @@ -1217,7 +1194,7 @@ class FunctionsCustomServerTest extends Scope { $functionId = $this->setupFunction([ 'functionId' => ID::unique(), - 'name' => 'timeout-php-8.0', + 'name' => 'Test php-8.0', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'events' => [], @@ -1612,8 +1589,8 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); $this->assertGreaterThan(0, $execution['body']['duration']); - $this->assertNotEmpty($execution['body']['responseBody']); - $this->assertStringContainsString("total", $execution['body']['responseBody']); + $this->assertNotEmpty($execution['body']['logs']); + $this->assertStringContainsString("total", $execution['body']['logs']); }, 10000, 500); $this->cleanupFunction($functionId); diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index bee15693f1..cbe54b6445 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -62,7 +62,7 @@ class FunctionsScheduleTest extends Scope $this->assertNotEmpty($asyncExecution['logs']); $this->assertNotEmpty($asyncExecution['errors']); $this->assertGreaterThan(0, $asyncExecution['duration']); - }, 15000, 500); + }, 60000, 500); $this->cleanupFunction($functionId); } From cefc7a1d4baf4ac670125d8ff491617092b6687d Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:44:34 +0100 Subject: [PATCH 158/241] chore: fmt --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index fec815c2ef..050c0681c5 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -796,7 +796,7 @@ class FunctionsCustomServerTest extends Scope $matchingDeployment = array_filter( $deployments['body']['deployments'], - fn($deployment) => $deployment['$id'] === $deploymentId + fn ($deployment) => $deployment['$id'] === $deploymentId ); $this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found"); From 42ae429153156a6b20d7fd92cfbef699e02631ef Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:49:29 +0100 Subject: [PATCH 159/241] chore: remove test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 050c0681c5..a0395a08b0 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -898,8 +898,6 @@ class FunctionsCustomServerTest extends Scope */ $executions = $this->listExecutions($data['functionId']); - fwrite(STDERR, print_r(json_encode($executions, JSON_PRETTY_PRINT), true)); - $this->assertEquals(200, $executions['headers']['status-code']); $this->assertEquals(1, $executions['body']['total']); $this->assertIsArray($executions['body']['executions']); From df9e80a9629c638a276f96717096e406b71f98c2 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:02:23 +0100 Subject: [PATCH 160/241] feat: handle bools --- tests/e2e/Client.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 0774f1c6fd..0595611570 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -303,6 +303,8 @@ class Client if (is_array($value)) { $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } elseif (is_bool($value)) { + $output[$finalKey] = $value ? 'true' : 'false'; } else { $output[$finalKey] = $value; } From f0485ca87010df935785a3c4a8c29c47ca3d034e Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:30:26 +0100 Subject: [PATCH 161/241] fix: use boolean false --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index a0395a08b0..9166a7c4e8 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -463,7 +463,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => 'false' + 'activate' => false ]); $this->assertEquals(202, $deployment['headers']['status-code']); @@ -502,7 +502,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => 'false' + 'activate' => false ]); $deploymentId = $deployment['body']['$id'] ?? ''; From 0eb0054f3818ea09e6ee71f9641f12dc26410393 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 22 Sep 2024 10:50:13 +0300 Subject: [PATCH 162/241] Add info resourceType --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index dc76f668e6..15ef0f4987 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -158,10 +158,10 @@ abstract class ScheduleBase extends Action $new = \strtotime($document['resourceUpdatedAt']); if (!$document['active']) { - Console::info("Removing: {$document['resourceId']}"); + Console::info("Removing: {$document['resourceType']}-{$document['resourceId']}"); unset($this->schedules[$document->getInternalId()]); } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceId']}"); + Console::info("Updating: {$document['resourceType']}-{$document['resourceId']}"); $this->schedules[$document->getInternalId()] = $getSchedule($document); } } From 8213b37cf428b6de15a9f90ca87e746984311856 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 22 Sep 2024 13:29:42 +0300 Subject: [PATCH 163/241] Add info resourceType --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 15ef0f4987..6e3459667f 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -158,10 +158,10 @@ abstract class ScheduleBase extends Action $new = \strtotime($document['resourceUpdatedAt']); if (!$document['active']) { - Console::info("Removing: {$document['resourceType']}-{$document['resourceId']}"); + Console::info("Removing: {$document['resourceType']}:{$document['resourceId']}"); unset($this->schedules[$document->getInternalId()]); } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceType']}-{$document['resourceId']}"); + Console::info("Updating: {$document['resourceType']}:{$document['resourceId']}"); $this->schedules[$document->getInternalId()] = $getSchedule($document); } } From 6f5b144e70f919b61e0008771e0cefd5ff1c6749 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 22 Sep 2024 14:15:01 +0300 Subject: [PATCH 164/241] Add info resourceType --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 6e3459667f..a1b85c341f 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -158,10 +158,10 @@ abstract class ScheduleBase extends Action $new = \strtotime($document['resourceUpdatedAt']); if (!$document['active']) { - Console::info("Removing: {$document['resourceType']}:{$document['resourceId']}"); + Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}"); unset($this->schedules[$document->getInternalId()]); } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceType']}:{$document['resourceId']}"); + Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}"); $this->schedules[$document->getInternalId()] = $getSchedule($document); } } From f6e3759f6b05cb87a446c982e94ce91518ed663a Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Sun, 22 Sep 2024 20:14:58 +0100 Subject: [PATCH 165/241] chore: add async --- tests/e2e/Services/Functions/FunctionsBase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index badab22147..15acce8ba2 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -2,12 +2,15 @@ namespace Tests\E2E\Services\Functions; +use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; trait FunctionsBase { + use Async; + protected string $stdout = ''; protected string $stderr = ''; From a95cd51be059023f239c6b575abd365f445e9f9b Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Sep 2024 11:20:38 +0300 Subject: [PATCH 166/241] destination false --- app/config/collections.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/collections.php b/app/config/collections.php index f6f5d7b7ef..79a1b659c5 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4122,7 +4122,7 @@ $projectCollections = array_merge([ 'format' => '', 'size' => 500, 'signed' => true, - 'required' => true, + 'required' => false, // make true after patch script 'default' => null, 'array' => false, 'filters' => [], From 831472fc64260b03c827b7f7a5d2f1f66a5668c6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 12:48:18 +0300 Subject: [PATCH 167/241] destination size --- app/config/collections.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 79a1b659c5..1eb286cf8f 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4109,7 +4109,7 @@ $projectCollections = array_merge([ '$id' => ID::custom('source'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 8192, + 'size' => 8192, // reduce size 'signed' => true, 'required' => true, 'default' => null, @@ -4120,7 +4120,7 @@ $projectCollections = array_merge([ '$id' => ID::custom('destination'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 500, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, // make true after patch script 'default' => null, From 77aad0bfce0026275e8759620b882586cde922aa Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 12:58:09 +0300 Subject: [PATCH 168/241] Bumb migration tag --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5bff5a62fa..7d261fbab7 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "dev-preserveDates as 0.6.0", + "utopia-php/migration": "0.6.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", From ab908aa9cb65c758d3218a2f11e18c8f2c1652c8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 18:20:00 +0300 Subject: [PATCH 169/241] Add const --- app/config/platforms.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/config/platforms.php b/app/config/platforms.php index e7eb1180cd..40cea19fd3 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -1,5 +1,9 @@ [ 'key' => APP_PLATFORM_CLIENT, From 3121cd1c07fe28b0ac35bda55f97d374cec59671 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Tue, 24 Sep 2024 18:52:38 +0300 Subject: [PATCH 170/241] Update src/Appwrite/Platform/Workers/Migrations.php Co-authored-by: Christy Jacob --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index c4c2516424..1fbbd6d16c 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -396,7 +396,7 @@ class Migrations extends Action $this->updateMigrationDocument($migration, $projectDocument); - if ($migration->getAttribute('status', '') == 'failed') { + if ($migration->getAttribute('status', '') === 'failed') { $destination->error(); $source->error(); From 75d681e6e208346e88737b3febabc10b6c2e3825 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 19:01:01 +0300 Subject: [PATCH 171/241] Remove Breadcrumb --- src/Appwrite/Platform/Workers/Migrations.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 1fbbd6d16c..08757d3e2a 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -287,7 +287,6 @@ class Migrations extends Action $migration->setAttribute('stage', 'processing'); $migration->setAttribute('status', 'processing'); - $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'processing'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); $log->addTag('type', $migration->getAttribute('source')); @@ -304,7 +303,6 @@ class Migrations extends Action /** Start Transfer */ $migration->setAttribute('stage', 'migrating'); - $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'migrating'", \microtime(true))); $this->updateMigrationDocument($migration, $projectDocument); $transfer->run( @@ -327,7 +325,6 @@ class Migrations extends Action if (! empty($sourceErrors) || ! empty($destinationErrors)) { $migration->setAttribute('status', 'failed'); $migration->setAttribute('stage', 'finished'); - $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'finished' and failed", \microtime(true))); $errorMessages = []; foreach ($sourceErrors as $error) { @@ -359,7 +356,6 @@ class Migrations extends Action $migration->setAttribute('status', 'completed'); $migration->setAttribute('stage', 'finished'); - $log->addBreadcrumb(new Breadcrumb('debug', 'migration', "Migration hit stage 'finished' and succeeded", \microtime(true))); } catch (\Throwable $th) { Console::error($th->getMessage()); Console::error($th->getTraceAsString()); From 3ff6ccbda1482f8a46e373a5cb9a19341c757c2c Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 19:18:05 +0300 Subject: [PATCH 172/241] Remove and add in init.php --- app/config/platforms.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index 40cea19fd3..e7eb1180cd 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -1,9 +1,5 @@ [ 'key' => APP_PLATFORM_CLIENT, From bd8335109217fea7c9150c1816a49d5384ce61a3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 19:18:40 +0300 Subject: [PATCH 173/241] lock.file --- composer.lock | 109 +++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 59 deletions(-) diff --git a/composer.lock b/composer.lock index d2a5d88fb9..4cd5853fd0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "67946bb84bca00b694c8a41fbe41f359", + "content-hash": "3a25a9050c489b1a2cb4e320af1586d5", "packages": [ { "name": "adhocore/jwt", @@ -2070,16 +2070,16 @@ }, { "name": "utopia-php/logger", - "version": "0.6.0", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9" + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/7e8ff512c6f04577aba1df67c7b9628971946f9c", + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c", "shasum": "" }, "require": { @@ -2118,9 +2118,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.6.0" + "source": "https://github.com/utopia-php/logger/tree/0.6.1" }, - "time": "2024-05-23T13:37:54+00:00" + "time": "2024-09-20T14:02:12+00:00" }, { "name": "utopia-php/messaging", @@ -2175,16 +2175,16 @@ }, { "name": "utopia-php/migration", - "version": "dev-preserveDates", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe" + "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe", - "reference": "c5ac9347448e34832ff0bd1f9d3d90c54cd6e2fe", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", + "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", "shasum": "" }, "require": { @@ -2225,9 +2225,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/preserveDates" + "source": "https://github.com/utopia-php/migration/tree/0.6.1" }, - "time": "2024-09-19T15:32:44+00:00" + "time": "2024-09-24T09:54:09+00:00" }, { "name": "utopia-php/mongo", @@ -3323,16 +3323,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "eb2bcbd85034c1a508cf409b7ee318eb3137f090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/eb2bcbd85034c1a508cf409b7ee318eb3137f090", + "reference": "eb2bcbd85034c1a508cf409b7ee318eb3137f090", "shasum": "" }, "require": { @@ -3385,7 +3385,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T15:48:42+00:00" }, { "name": "matthiasmullie/minify", @@ -4194,16 +4194,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.1", + "version": "1.31.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e" + "reference": "249f15fb843bf240cf058372dad29e100cee6c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", + "reference": "249f15fb843bf240cf058372dad29e100cee6c17", "shasum": "" }, "require": { @@ -4235,9 +4235,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" }, - "time": "2024-09-07T20:13:05+00:00" + "time": "2024-09-22T11:32:18+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5874,16 +5874,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -5947,7 +5947,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -5963,7 +5963,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6034,16 +6034,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", "shasum": "" }, "require": { @@ -6080,7 +6080,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.2" + "source": "https://github.com/symfony/filesystem/tree/v7.1.5" }, "funding": [ { @@ -6096,7 +6096,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/finder", @@ -6545,16 +6545,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -6586,7 +6586,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -6602,7 +6602,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/service-contracts", @@ -6689,16 +6689,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -6756,7 +6756,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -6772,7 +6772,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "textalk/websocket", @@ -7002,18 +7002,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/migration", - "version": "dev-preserveDates", - "alias": "0.6.0", - "alias_normalized": "0.6.0.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { From a2c1ce4e8f2516903a0d20dcc8bb331dd4961c58 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 19:39:39 +0300 Subject: [PATCH 174/241] specs --- .../specs/open-api3-latest-console.json | 140 +++++++++++++++++- app/config/specs/open-api3-latest-server.json | 140 +++++++++++++++++- app/config/specs/swagger2-latest-console.json | 140 +++++++++++++++++- app/config/specs/swagger2-latest-server.json | 140 +++++++++++++++++- 4 files changed, 544 insertions(+), 16 deletions(-) diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 07749889d8..86a6ee7e53 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -32946,6 +32946,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -32965,6 +32975,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -33003,6 +33015,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -33030,7 +33052,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -33068,6 +33092,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -33095,7 +33129,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -33133,6 +33169,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -33145,7 +33191,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -33183,6 +33231,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33201,6 +33259,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33239,6 +33299,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -33265,6 +33335,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -33304,6 +33376,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33322,6 +33404,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33360,6 +33444,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33378,6 +33472,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33416,6 +33512,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -33434,6 +33540,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33472,6 +33580,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -33509,6 +33627,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -33557,6 +33677,16 @@ }, "x-example": [], "nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -33564,7 +33694,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index ef41688ca8..84c6fe1454 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -23677,6 +23677,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -23696,6 +23706,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -23734,6 +23746,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -23761,7 +23783,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -23799,6 +23823,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -23826,7 +23860,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -23864,6 +23900,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -23876,7 +23922,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -23914,6 +23962,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -23932,6 +23990,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -23970,6 +24030,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -23996,6 +24066,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -24035,6 +24107,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24053,6 +24135,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24091,6 +24175,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24109,6 +24203,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24147,6 +24243,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -24165,6 +24271,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24203,6 +24311,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -24240,6 +24358,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -24288,6 +24408,16 @@ }, "x-example": [], "nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -24295,7 +24425,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 51935a5e01..49d60e980b 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -33456,6 +33456,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -33475,6 +33485,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -33513,6 +33525,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -33540,7 +33562,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -33578,6 +33602,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -33605,7 +33639,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -33643,6 +33679,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -33655,7 +33701,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -33693,6 +33741,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33711,6 +33769,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33749,6 +33809,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -33775,6 +33845,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -33814,6 +33886,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33832,6 +33914,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33870,6 +33954,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33888,6 +33982,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33926,6 +34022,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -33944,6 +34050,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33982,6 +34090,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -34019,6 +34137,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -34067,6 +34187,16 @@ }, "x-example": [], "x-nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -34074,7 +34204,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index af6274226f..bc22585860 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -24166,6 +24166,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -24185,6 +24195,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -24223,6 +24235,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -24250,7 +24272,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -24288,6 +24312,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -24315,7 +24349,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -24353,6 +24389,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -24365,7 +24411,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -24403,6 +24451,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24421,6 +24479,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24459,6 +24519,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -24485,6 +24555,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -24524,6 +24596,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24542,6 +24624,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24580,6 +24664,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24598,6 +24692,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24636,6 +24732,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -24654,6 +24760,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24692,6 +24800,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -24729,6 +24847,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -24777,6 +24897,16 @@ }, "x-example": [], "x-nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -24784,7 +24914,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { From d532c874d52333402fca9f4ae99332ea61827297 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 19:43:05 +0300 Subject: [PATCH 175/241] specs 1.6.x --- app/config/specs/open-api3-1.6.x-console.json | 140 +++++++++++++++++- app/config/specs/open-api3-1.6.x-server.json | 140 +++++++++++++++++- app/config/specs/swagger2-1.6.x-console.json | 140 +++++++++++++++++- app/config/specs/swagger2-1.6.x-server.json | 140 +++++++++++++++++- 4 files changed, 544 insertions(+), 16 deletions(-) diff --git a/app/config/specs/open-api3-1.6.x-console.json b/app/config/specs/open-api3-1.6.x-console.json index 07749889d8..86a6ee7e53 100644 --- a/app/config/specs/open-api3-1.6.x-console.json +++ b/app/config/specs/open-api3-1.6.x-console.json @@ -32946,6 +32946,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -32965,6 +32975,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -33003,6 +33015,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -33030,7 +33052,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -33068,6 +33092,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -33095,7 +33129,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -33133,6 +33169,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -33145,7 +33191,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -33183,6 +33231,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33201,6 +33259,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33239,6 +33299,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -33265,6 +33335,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -33304,6 +33376,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33322,6 +33404,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33360,6 +33444,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33378,6 +33472,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33416,6 +33512,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -33434,6 +33540,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33472,6 +33580,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -33509,6 +33627,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -33557,6 +33677,16 @@ }, "x-example": [], "nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -33564,7 +33694,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/open-api3-1.6.x-server.json b/app/config/specs/open-api3-1.6.x-server.json index ef41688ca8..84c6fe1454 100644 --- a/app/config/specs/open-api3-1.6.x-server.json +++ b/app/config/specs/open-api3-1.6.x-server.json @@ -23677,6 +23677,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -23696,6 +23706,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -23734,6 +23746,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -23761,7 +23783,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -23799,6 +23823,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -23826,7 +23860,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -23864,6 +23900,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -23876,7 +23922,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -23914,6 +23962,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -23932,6 +23990,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -23970,6 +24030,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -23996,6 +24066,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -24035,6 +24107,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24053,6 +24135,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24091,6 +24175,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24109,6 +24203,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24147,6 +24243,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -24165,6 +24271,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24203,6 +24311,16 @@ "x-example": false, "nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -24240,6 +24358,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -24288,6 +24408,16 @@ }, "x-example": [], "nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -24295,7 +24425,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/swagger2-1.6.x-console.json b/app/config/specs/swagger2-1.6.x-console.json index 51935a5e01..49d60e980b 100644 --- a/app/config/specs/swagger2-1.6.x-console.json +++ b/app/config/specs/swagger2-1.6.x-console.json @@ -33456,6 +33456,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -33475,6 +33485,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -33513,6 +33525,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -33540,7 +33562,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -33578,6 +33602,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -33605,7 +33639,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -33643,6 +33679,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -33655,7 +33701,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -33693,6 +33741,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33711,6 +33769,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33749,6 +33809,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -33775,6 +33845,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -33814,6 +33886,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33832,6 +33914,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33870,6 +33954,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -33888,6 +33982,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33926,6 +34022,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -33944,6 +34050,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -33982,6 +34090,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -34019,6 +34137,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -34067,6 +34187,16 @@ }, "x-example": [], "x-nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -34074,7 +34204,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { diff --git a/app/config/specs/swagger2-1.6.x-server.json b/app/config/specs/swagger2-1.6.x-server.json index af6274226f..bc22585860 100644 --- a/app/config/specs/swagger2-1.6.x-server.json +++ b/app/config/specs/swagger2-1.6.x-server.json @@ -24166,6 +24166,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "size": { "type": "integer", "description": "Attribute size.", @@ -24185,6 +24195,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "size" ] }, @@ -24223,6 +24235,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "integer", "description": "Minimum value to enforce for new documents.", @@ -24250,7 +24272,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeFloat": { @@ -24288,6 +24312,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "min": { "type": "number", "description": "Minimum value to enforce for new documents.", @@ -24315,7 +24349,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeBoolean": { @@ -24353,6 +24389,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "default": { "type": "boolean", "description": "Default value for attribute when not provided. Cannot be set when attribute is required.", @@ -24365,7 +24411,9 @@ "type", "status", "error", - "required" + "required", + "$createdAt", + "$updatedAt" ] }, "attributeEmail": { @@ -24403,6 +24451,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24421,6 +24479,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24459,6 +24519,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "elements": { "type": "array", "description": "Array of elements in enumerated type.", @@ -24485,6 +24555,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "elements", "format" ] @@ -24524,6 +24596,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24542,6 +24624,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24580,6 +24664,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "String format.", @@ -24598,6 +24692,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24636,6 +24732,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "format": { "type": "string", "description": "ISO 8601 format.", @@ -24654,6 +24760,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "format" ] }, @@ -24692,6 +24800,16 @@ "x-example": false, "x-nullable": true }, + "$createdAt": { + "type": "string", + "description": "Attribute creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Attribute update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, "relatedCollection": { "type": "string", "description": "The ID of the related collection.", @@ -24729,6 +24847,8 @@ "status", "error", "required", + "$createdAt", + "$updatedAt", "relatedCollection", "relationType", "twoWay", @@ -24777,6 +24897,16 @@ }, "x-example": [], "x-nullable": true + }, + "$createdAt": { + "type": "string", + "description": "Index creation date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Index update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" } }, "required": [ @@ -24784,7 +24914,9 @@ "type", "status", "error", - "attributes" + "attributes", + "$createdAt", + "$updatedAt" ] }, "document": { From 12d119069e35d299619ebf65e5a4c32556747587 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 22:02:18 +0300 Subject: [PATCH 176/241] equal --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 08757d3e2a..9e58310733 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -392,7 +392,7 @@ class Migrations extends Action $this->updateMigrationDocument($migration, $projectDocument); - if ($migration->getAttribute('status', '') === 'failed') { + if ($migration->getAttribute('status', '') == 'failed') { $destination->error(); $source->error(); From a42a669d6e0e7e1c480edff143edf56083d79a54 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Sep 2024 22:02:38 +0300 Subject: [PATCH 177/241] equal equal --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 9e58310733..08757d3e2a 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -392,7 +392,7 @@ class Migrations extends Action $this->updateMigrationDocument($migration, $projectDocument); - if ($migration->getAttribute('status', '') == 'failed') { + if ($migration->getAttribute('status', '') === 'failed') { $destination->error(); $source->error(); From 0753163066d24591688bee6f095acf12703c4d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 25 Sep 2024 13:32:09 +0200 Subject: [PATCH 178/241] PR review changes --- tests/e2e/Client.php | 2 -- tests/e2e/Services/Functions/FunctionsBase.php | 11 ++++------- tests/e2e/Services/GraphQL/Base.php | 6 ++---- tests/extensions/Async/Eventually.php | 7 +------ 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 0595611570..0774f1c6fd 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -303,8 +303,6 @@ class Client if (is_array($value)) { $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed - } elseif (is_bool($value)) { - $output[$finalKey] = $value ? 'true' : 'false'; } else { $output[$finalKey] = $value; } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 15acce8ba2..a1bb8f2b21 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -142,14 +142,12 @@ trait FunctionsBase return $executions; } - protected function packageFunction(string $function = 'php'): CURLFile + protected function packageFunction(string $function): CURLFile { $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; $tarPath = "$folderPath/code.tar.gz"; - if (!file_exists($tarPath)) { - Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); - } + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); if (filesize($tarPath) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); @@ -200,11 +198,10 @@ trait FunctionsBase protected function deleteFunction(string $functionId): mixed { - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + ], $this->getHeaders())); return $function; } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 01f9508a81..0b3250cecf 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2497,14 +2497,12 @@ trait Base protected string $stdout = ''; protected string $stderr = ''; - protected function packageFunction(string $function = 'php'): CURLFile + protected function packageFunction(string $function): CURLFile { $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; $tarPath = "$folderPath/code.tar.gz"; - if (!file_exists($tarPath)) { - Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); - } + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); if (filesize($tarPath) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); diff --git a/tests/extensions/Async/Eventually.php b/tests/extensions/Async/Eventually.php index e4987de410..9840b1a114 100644 --- a/tests/extensions/Async/Eventually.php +++ b/tests/extensions/Async/Eventually.php @@ -6,13 +6,8 @@ use PHPUnit\Framework\Constraint\Constraint; final class Eventually extends Constraint { - private int $timeoutMs; - private int $waitMs; - - public function __construct(int $timeoutMs, int $waitMs) + public function __construct(private int $timeoutMs, private int $waitMs) { - $this->timeoutMs = $timeoutMs; - $this->waitMs = $waitMs; } public function evaluate(mixed $probe, string $description = '', bool $returnResult = false): ?bool From 5f97aafe4604e535cf437115d82f9db000acbdf8 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:38:30 +0530 Subject: [PATCH 179/241] More asserts --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 4dabec67f0..f101524aaf 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2669,8 +2669,9 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } - public function testCreateFunctionWithResponseFormatHeader() + public function testResponseFilters() { + // create function with 1.5.0 response format $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2684,8 +2685,10 @@ class FunctionsCustomServerTest extends Scope ]); $this->assertEquals(201, $response['headers']['status-code']); + $this->assertArrayNotHasKey('scopes', $response['body']); + $this->assertArrayNotHasKey('specification', $response['body']); - // get function with 1.5.0 response format + // get function with 1.5.0 response format header $function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 71ffe2bf5234d595b18d60464e8bdbb032f33d16 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 16:39:31 +0300 Subject: [PATCH 180/241] Deletes schedules --- src/Appwrite/Platform/Workers/Deletes.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7b7f5e1fa6..c69b8a1bbd 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -557,6 +557,11 @@ class Deletes extends Action Query::equal('projectInternalId', [$projectInternalId]), ], $dbForConsole); + // Delete Schedules + $this->deleteByGroup('schedules', [ + Query::equal('projectInternalId', [$projectInternalId]), + ], $dbForConsole); + // Delete metadata table if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { $dbForProject->deleteCollection('_metadata'); From a9469e87344f7b30f4b0f6281dadbf2d94b26586 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 16:49:49 +0300 Subject: [PATCH 181/241] projectId schedules --- src/Appwrite/Platform/Workers/Deletes.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index c69b8a1bbd..8b835c53f3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -480,6 +480,7 @@ class Deletes extends Action private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void { $projectInternalId = $document->getInternalId(); + $projectId = $document->getId(); try { $dsn = new DSN($document->getAttribute('database', 'console')); @@ -557,9 +558,9 @@ class Deletes extends Action Query::equal('projectInternalId', [$projectInternalId]), ], $dbForConsole); - // Delete Schedules + // Delete Schedules (No projectInternalId in this collection) $this->deleteByGroup('schedules', [ - Query::equal('projectInternalId', [$projectInternalId]), + Query::equal('projectId', [$projectId]), ], $dbForConsole); // Delete metadata table From 7116405793d6a9795ae7a44f2ba90772e3c0d4c1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 18:16:57 +0300 Subject: [PATCH 182/241] Dbg $projectCollectionIds --- src/Appwrite/Platform/Workers/Deletes.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 8b835c53f3..022c217682 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -496,7 +496,7 @@ class Deletes extends Action Audit::COLLECTION, TimeLimit::COLLECTION, ]; - +var_dump($projectCollectionIds); $limit = \count($projectCollectionIds) + 25; while (true) { @@ -559,6 +559,7 @@ class Deletes extends Action ], $dbForConsole); // Delete Schedules (No projectInternalId in this collection) + var_dump("deleting schedules projectId = " . $projectId); $this->deleteByGroup('schedules', [ Query::equal('projectId', [$projectId]), ], $dbForConsole); From 868c4162023cfaea1faddebb4ebb4b0e88af7348 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 18:19:41 +0300 Subject: [PATCH 183/241] Dbg $projectCollectionIds --- src/Appwrite/Platform/Workers/Deletes.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 022c217682..a2abd91814 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -496,13 +496,16 @@ class Deletes extends Action Audit::COLLECTION, TimeLimit::COLLECTION, ]; -var_dump($projectCollectionIds); + $limit = \count($projectCollectionIds) + 25; while (true) { $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { + + var_dump($collection->getId()); + if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); } else { From 3228ccb4f9088b9b3ba33aa08e0f472d54a4376b Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 18:21:47 +0300 Subject: [PATCH 184/241] Dbg $projectCollectionIds --- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index a2abd91814..af99c7e37d 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -496,7 +496,7 @@ class Deletes extends Action Audit::COLLECTION, TimeLimit::COLLECTION, ]; - + var_dump($projectCollectionIds); $limit = \count($projectCollectionIds) + 25; while (true) { From 0ace58f00bdc60ceaf36065cdbfb05ec012123d0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 19:04:47 +0300 Subject: [PATCH 185/241] Dbg $projectCollectionIds --- src/Appwrite/Platform/Workers/Deletes.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index af99c7e37d..de90f4a5f3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -507,7 +507,13 @@ class Deletes extends Action var_dump($collection->getId()); if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { + var_dump('start ' . $collection->getId()); + $dbForProject->deleteCollection($collection->getId()); + + var_dump('finish ' . $collection->getId()); + var_dump(''); + } else { $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } From 1cf26b1f4600b0aa4c0f7dad7f5d053c63a216cc Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 25 Sep 2024 19:08:24 +0300 Subject: [PATCH 186/241] Dbg $projectCollectionIds --- src/Appwrite/Platform/Workers/Deletes.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index de90f4a5f3..7400a43b93 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -503,9 +503,6 @@ class Deletes extends Action $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { - - var_dump($collection->getId()); - if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { var_dump('start ' . $collection->getId()); @@ -515,6 +512,8 @@ class Deletes extends Action var_dump(''); } else { + + var_dump('deleteByGroup' . $collection->getId()); $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } } From fdf73f8a7ae7129f5a82a235bcf18f5e337d9e23 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:36:00 +0530 Subject: [PATCH 187/241] Add test for request filters --- .../Functions/FunctionsCustomServerTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f101524aaf..d4adc1119f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2719,6 +2719,55 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + public function testRequestFilters() + { + // create function + $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 15, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // create another function + $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test2', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 15, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // list functions using request filters + $response = $this->client->call( + Client::METHOD_GET, + '/functions', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax + ], $this->getHeaders()), + [ + 'queries' => [ 'equal("name", ["Test2"])' ] + ] + ); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['functions']); + $this->assertEquals('Test2', $response['body']['functions'][0]['name']); + } + public function testFunctionLogging() { // Preparations: Create Function From 17d0053f37b917b23e5df63cedc3cd668ae4fe61 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 09:45:00 +0300 Subject: [PATCH 188/241] use Authorization skip --- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7400a43b93..58f326cae3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -506,7 +506,7 @@ class Deletes extends Action if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { var_dump('start ' . $collection->getId()); - $dbForProject->deleteCollection($collection->getId()); + \Utopia\Database\Validator\Authorization::skip(fn () => $dbForProject->deleteCollection($collection->getId())); var_dump('finish ' . $collection->getId()); var_dump(''); From 5c7e4393a7c4cbd9d00e0e43c3ae2d0a8e10cd42 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 09:54:26 +0300 Subject: [PATCH 189/241] revert --- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 58f326cae3..7400a43b93 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -506,7 +506,7 @@ class Deletes extends Action if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { var_dump('start ' . $collection->getId()); - \Utopia\Database\Validator\Authorization::skip(fn () => $dbForProject->deleteCollection($collection->getId())); + $dbForProject->deleteCollection($collection->getId()); var_dump('finish ' . $collection->getId()); var_dump(''); From 226c3c0372faf129799758c39c5d80371ce13cc9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 10:38:36 +0300 Subject: [PATCH 190/241] Remove var_dumps --- src/Appwrite/Platform/Workers/Deletes.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7400a43b93..8b835c53f3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -496,7 +496,7 @@ class Deletes extends Action Audit::COLLECTION, TimeLimit::COLLECTION, ]; - var_dump($projectCollectionIds); + $limit = \count($projectCollectionIds) + 25; while (true) { @@ -504,16 +504,8 @@ class Deletes extends Action foreach ($collections as $collection) { if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { - var_dump('start ' . $collection->getId()); - $dbForProject->deleteCollection($collection->getId()); - - var_dump('finish ' . $collection->getId()); - var_dump(''); - } else { - - var_dump('deleteByGroup' . $collection->getId()); $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } } @@ -567,7 +559,6 @@ class Deletes extends Action ], $dbForConsole); // Delete Schedules (No projectInternalId in this collection) - var_dump("deleting schedules projectId = " . $projectId); $this->deleteByGroup('schedules', [ Query::equal('projectId', [$projectId]), ], $dbForConsole); From 672c8b2730dc00e9e6a2e04fdfd0beedcf64e013 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 10:52:53 +0300 Subject: [PATCH 191/241] lock file --- composer.lock | 71 +++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/composer.lock b/composer.lock index b3a94545b4..268995100b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7066d9ca32e7a1a60614effdc4701970", + "content-hash": "18b1ca2bf504854287ba8a490b63b597", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -2205,27 +2206,35 @@ }, { "name": "utopia-php/migration", - "version": "0.5.2", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a" + "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f18d44d4459f78c292dac9edde856fd156fe497a", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", + "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "11.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3.*", + "utopia-php/database": "0.53.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "0.33.*", + "utopia-php/storage": "0.18.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", - "vlucas/phpdotenv": "5.*" + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.16.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2247,9 +2256,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.2" + "source": "https://github.com/utopia-php/migration/tree/0.6.1" }, - "time": "2024-07-22T09:27:07+00:00" + "time": "2024-09-24T09:54:09+00:00" }, { "name": "utopia-php/mongo", @@ -3345,16 +3354,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -3407,7 +3416,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T17:22:50+00:00" }, { "name": "matthiasmullie/minify", @@ -4216,16 +4225,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.31.0", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -4257,9 +4266,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-09-22T11:32:18+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpunit/php-code-coverage", From 2b08fb59c341ac9d5c7f0a7ea0a181915fd4f010 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 16:50:42 +0300 Subject: [PATCH 192/241] relations --- src/Appwrite/Platform/Workers/Deletes.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 8b835c53f3..988a8f2e48 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -503,6 +503,25 @@ class Deletes extends Action $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { + + /** + * Ignore junction tables; + */ + $relationships = \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => + $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP && + $attribute->getAttribute('options')['side'] === Database::RELATION_SIDE_PARENT && + $attribute->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY + ); + + $junctions = []; + foreach ($relationships as $relationship) { + var_dump("many2many"); + var_dump($collection->getInternalId()); + var_dump($relationship); + } + if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); } else { From e000234dbd92c02f3bc6da2d14cbac2a7718d720 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 18:11:41 +0300 Subject: [PATCH 193/241] relatedCollection --- src/Appwrite/Platform/Workers/Deletes.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 988a8f2e48..3ffd29f3a1 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -517,11 +517,18 @@ class Deletes extends Action $junctions = []; foreach ($relationships as $relationship) { + $relatedCollection = $relationship->getAttribute('options')['relatedCollection']; var_dump("many2many"); + var_dump("relatedCollection"); var_dump($collection->getInternalId()); + var_dump($relatedCollection->getInternalId()); var_dump($relationship); + $x = "_{$relatedCollection->getInternalId()}_{$collection->getInternalId()}"; + var_dump($x); } + $junctions = []; + if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); } else { From d642111bfa48480a09adaf1a9747437a32f40a14 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 18:13:27 +0300 Subject: [PATCH 194/241] relatedCollection --- src/Appwrite/Platform/Workers/Deletes.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 3ffd29f3a1..7fda80d670 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -498,6 +498,7 @@ class Deletes extends Action ]; $limit = \count($projectCollectionIds) + 25; + $junctions = []; while (true) { $collections = $dbForProject->listCollections($limit); @@ -515,7 +516,6 @@ class Deletes extends Action $attribute->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY ); - $junctions = []; foreach ($relationships as $relationship) { $relatedCollection = $relationship->getAttribute('options')['relatedCollection']; var_dump("many2many"); @@ -524,10 +524,11 @@ class Deletes extends Action var_dump($relatedCollection->getInternalId()); var_dump($relationship); $x = "_{$relatedCollection->getInternalId()}_{$collection->getInternalId()}"; + $junctions[] = "_{$relatedCollection->getInternalId()}_{$collection->getInternalId()}"; var_dump($x); } - $junctions = []; + var_dump($junctions); if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); From a8bb54a33ed8df36a0e1f421b6f8201206d4895a Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Sep 2024 18:24:40 +0300 Subject: [PATCH 195/241] var_dump $junctions --- src/Appwrite/Platform/Workers/Deletes.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7fda80d670..f90e9d34ec 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -504,7 +504,6 @@ class Deletes extends Action $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { - /** * Ignore junction tables; */ From 26f0efc34defb5d9fde6e32d9b8c87e7f42e18f3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Fri, 27 Sep 2024 10:45:23 +0300 Subject: [PATCH 196/241] Ignore junction tables --- src/Appwrite/Platform/Workers/Deletes.php | 38 +++++++---------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index f90e9d34ec..c22d23087f 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -504,33 +504,19 @@ class Deletes extends Action $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { - /** - * Ignore junction tables; - */ - $relationships = \array_filter( - $collection->getAttribute('attributes', []), - fn ($attribute) => - $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP && - $attribute->getAttribute('options')['side'] === Database::RELATION_SIDE_PARENT && - $attribute->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY - ); - - foreach ($relationships as $relationship) { - $relatedCollection = $relationship->getAttribute('options')['relatedCollection']; - var_dump("many2many"); - var_dump("relatedCollection"); - var_dump($collection->getInternalId()); - var_dump($relatedCollection->getInternalId()); - var_dump($relationship); - $x = "_{$relatedCollection->getInternalId()}_{$collection->getInternalId()}"; - $junctions[] = "_{$relatedCollection->getInternalId()}_{$collection->getInternalId()}"; - var_dump($x); - } - - var_dump($junctions); - if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { - $dbForProject->deleteCollection($collection->getId()); + try { + $dbForProject->deleteCollection($collection->getId()); + } catch (DatabaseException $e) { + Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); + + /** + * Ignore junction tables; + */ + if (!preg_match('/^_\d+_\d+$/', $collection->getId())) { + throw $e; + } + } } else { $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } From 7389c9a5fe1f918b83e4f5aee0baf7a0c3fc55ae Mon Sep 17 00:00:00 2001 From: fogelito Date: Fri, 27 Sep 2024 10:50:09 +0300 Subject: [PATCH 197/241] Catch Throwable --- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index c22d23087f..4c589f4485 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -507,7 +507,7 @@ class Deletes extends Action if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { try { $dbForProject->deleteCollection($collection->getId()); - } catch (DatabaseException $e) { + } catch (Throwable $e) { Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); /** From e627cab92e8f0a08a42af2c1293c7ee7e532cfc8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Fri, 27 Sep 2024 11:43:22 +0300 Subject: [PATCH 198/241] Check deviceForFiles --- src/Appwrite/Platform/Workers/Deletes.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 4c589f4485..306e2abba9 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -582,6 +582,9 @@ class Deletes extends Action $this->deleteByGroup('_metadata', [], $dbForProject); } + + Console::error('deviceForFiles === ' . $deviceForFiles->getRoot()); + // Delete all storage directories $deviceForFiles->delete($deviceForFiles->getRoot(), true); $deviceForFunctions->delete($deviceForFunctions->getRoot(), true); From d5bf134c927020c3de8662756f5c2ee70236d09a Mon Sep 17 00:00:00 2001 From: fogelito Date: Fri, 27 Sep 2024 13:14:38 +0300 Subject: [PATCH 199/241] delete deleteCollection --- src/Appwrite/Platform/Workers/Deletes.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 306e2abba9..8b835c53f3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -498,25 +498,13 @@ class Deletes extends Action ]; $limit = \count($projectCollectionIds) + 25; - $junctions = []; while (true) { $collections = $dbForProject->listCollections($limit); foreach ($collections as $collection) { if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { - try { - $dbForProject->deleteCollection($collection->getId()); - } catch (Throwable $e) { - Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); - - /** - * Ignore junction tables; - */ - if (!preg_match('/^_\d+_\d+$/', $collection->getId())) { - throw $e; - } - } + $dbForProject->deleteCollection($collection->getId()); } else { $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } @@ -582,9 +570,6 @@ class Deletes extends Action $this->deleteByGroup('_metadata', [], $dbForProject); } - - Console::error('deviceForFiles === ' . $deviceForFiles->getRoot()); - // Delete all storage directories $deviceForFiles->delete($deviceForFiles->getRoot(), true); $deviceForFunctions->delete($deviceForFunctions->getRoot(), true); From 032fec3cae4591fbf1fb592a70b7798c9bf19ac3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 30 Sep 2024 10:43:50 +0300 Subject: [PATCH 200/241] Add id --- src/Appwrite/Platform/Workers/Migrations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 08757d3e2a..2747b85580 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -396,7 +396,7 @@ class Migrations extends Action $destination->error(); $source->error(); - throw new Exception('Migration failed'); + throw new Exception('Migration('.$migration->getId().') failed, Project('.$this->project->getId().')'); } } } From 6625d4e4a83a063f081f258f58043dc2f85b7fb9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 30 Sep 2024 11:01:01 +0300 Subject: [PATCH 201/241] Add console error --- src/Appwrite/Platform/Workers/Migrations.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 2747b85580..863548f0f3 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -393,10 +393,12 @@ class Migrations extends Action $this->updateMigrationDocument($migration, $projectDocument); if ($migration->getAttribute('status', '') === 'failed') { + Console::error('Migration('.$migration->getInternalId().':'.$migration->getId().') failed, Project('.$this->project->getInternalId().':'.$this->project->getId().')'); + $destination->error(); $source->error(); - throw new Exception('Migration('.$migration->getId().') failed, Project('.$this->project->getId().')'); + throw new Exception('Migration failed'); } } } From 05bcb92f73a34d2c175aba50151ec52502ee37c1 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:45:06 +0100 Subject: [PATCH 202/241] test: extra sleep --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4bc7b18d23..6e7012527d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -144,7 +144,7 @@ jobs: run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose up -d - sleep 25 + sleep 30 - name: Run ${{matrix.service}} Tests run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug From bb20b3f52c16b3b72e00397bbe45bc6c9a7eed94 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 30 Sep 2024 16:32:50 +0200 Subject: [PATCH 203/241] feat(realtime): projects channels --- src/Appwrite/Messaging/Adapter/Realtime.php | 5 +++ src/Appwrite/Platform/Workers/Migrations.php | 5 +-- .../Specification/Format/Swagger2.php | 2 +- .../Realtime/RealtimeConsoleClientTest.php | 43 +++++++++++++------ 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 49aa305c3f..d0d4a7c725 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -266,6 +266,7 @@ class Realtime extends Adapter break; case 'rules': $channels[] = 'console'; + $channels[] = 'projects.' . $project->getId(); $projectId = 'console'; $roles = [Role::team($project->getAttribute('teamId'))->toString()]; break; @@ -284,6 +285,7 @@ class Realtime extends Adapter case 'databases': if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) { $channels[] = 'console'; + $channels[] = 'projects.' . $project->getId(); $projectId = 'console'; $roles = [Role::team($project->getAttribute('teamId'))->toString()]; } elseif (($parts[4] ?? '') === 'documents') { @@ -323,6 +325,7 @@ class Realtime extends Adapter if ($parts[2] === 'executions') { if (!empty($payload->getRead())) { $channels[] = 'console'; + $channels[] = 'projects.' . $project->getId(); $channels[] = 'executions'; $channels[] = 'executions.' . $payload->getId(); $channels[] = 'functions.' . $payload->getAttribute('functionId'); @@ -330,6 +333,7 @@ class Realtime extends Adapter } } elseif ($parts[2] === 'deployments') { $channels[] = 'console'; + $channels[] = 'projects.' . $project->getId(); $projectId = 'console'; $roles = [Role::team($project->getAttribute('teamId'))->toString()]; } @@ -337,6 +341,7 @@ class Realtime extends Adapter break; case 'migrations': $channels[] = 'console'; + $channels[] = 'projects.' . $project->getId(); $projectId = 'console'; $roles = [Role::team($project->getAttribute('teamId'))->toString()]; break; diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 863548f0f3..c25d1b59e4 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -17,7 +17,6 @@ use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Logger\Log; -use Utopia\Logger\Log\Breadcrumb; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Exception as MigrationException; @@ -330,7 +329,7 @@ class Migrations extends Action foreach ($sourceErrors as $error) { /** @var $sourceErrors $error */ $message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'"; - if($error->getPrevious()){ + if ($error->getPrevious()) { $message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); } @@ -339,7 +338,7 @@ class Migrations extends Action foreach ($destinationErrors as $error) { $message = "Error occurred while pushing '{$error->getResourceName()}:{$error->getResourceId()}' to destination with message: '{$error->getMessage()}'"; - if($error->getPrevious()){ + if ($error->getPrevious()) { $message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine(); } diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 826bee83e6..15e3fdc2f4 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -300,7 +300,7 @@ class Swagger2 extends Format break; } - if($class === 'Utopia\Validator\AnyOf') { + if ($class === 'Utopia\Validator\AnyOf') { $validator = $param['validator']->getValidators()[0]; $class = \get_class($validator); } diff --git a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php index 6ab2874f8e..d4054ab8e9 100644 --- a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php @@ -19,7 +19,7 @@ class RealtimeConsoleClientTest extends Scope use ProjectCustom; use SideConsole; - public function testManualAuthentication() + public function testManualAuthentication(): void { $user = $this->getUser(); $userId = $user['$id'] ?? ''; @@ -124,7 +124,7 @@ class RealtimeConsoleClientTest extends Scope $client->close(); } - public function testAttributes() + public function testAttributes(): array { $user = $this->getUser(); $projectId = 'console'; @@ -184,6 +184,7 @@ class RealtimeConsoleClientTest extends Scope 'required' => true, ]); + $projectId = $this->getProject()['$id']; $attributeKey = $name['body']['key']; $this->assertEquals($name['headers']['status-code'], 202); @@ -198,8 +199,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*.create", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -218,8 +220,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -276,6 +279,8 @@ class RealtimeConsoleClientTest extends Scope ]); $this->assertEquals($index['headers']['status-code'], 202); + + $projectId = $this->getProject()['$id']; $indexKey = $index['body']['key']; $response = json_decode($client->receive(), true); @@ -285,8 +290,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*.create", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -303,8 +309,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -343,6 +350,8 @@ class RealtimeConsoleClientTest extends Scope $this->assertContains('console', $response['data']['channels']); $this->assertNotEmpty($response['data']['user']); + $projectId = $this->getProject()['$id']; + /** * Test Delete Index */ @@ -353,6 +362,7 @@ class RealtimeConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals($attribute['headers']['status-code'], 204); + $response = json_decode($client->receive(), true); $this->assertArrayHasKey('type', $response); @@ -360,8 +370,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -377,8 +388,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*.delete", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -416,10 +428,12 @@ class RealtimeConsoleClientTest extends Scope $this->assertContains('console', $response['data']['channels']); $this->assertNotEmpty($response['data']['user']); + $attributeKey = 'name'; + $projectId = $this->getProject()['$id']; + /** * Test Delete Attribute */ - $attributeKey = 'name'; $attribute = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['actorsId'] . '/attributes/' . $attributeKey, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -433,8 +447,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -450,8 +465,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*.delete", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); @@ -505,10 +521,10 @@ class RealtimeConsoleClientTest extends Scope /** * Test Create Deployment */ - $folder = 'php'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; $this->packageCode($folder); + $projectId = $this->getProject()['$id']; $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', @@ -530,8 +546,9 @@ class RealtimeConsoleClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(1, $response['data']['channels']); + $this->assertCount(2, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$projectId}", $response['data']['channels']); // $this->assertContains("functions.{$functionId}.deployments.{$deploymentId}.create", $response['data']['events']); TODO @christyjacob4 : enable test once we allow functions.* events $this->assertNotEmpty($response['data']['payload']); From 333682c4dc8eb9e930eeb4090991fbbb91e89654 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 30 Sep 2024 16:53:25 +0200 Subject: [PATCH 204/241] tests: add projects channel assertions --- src/Appwrite/Platform/Workers/Functions.php | 3 ++- tests/e2e/Services/Realtime/RealtimeCustomClientTest.php | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index e60f67416d..dfe7435426 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -587,7 +587,8 @@ class Functions extends Action $target = Realtime::fromPayload( // Pass first, most verbose event pattern event: $allEvents[0], - payload: $execution + payload: $execution, + project: $project ); Realtime::send( projectId: 'console', diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index c3372b98c5..d526afe13a 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1340,8 +1340,9 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(4, $response['data']['channels']); + $this->assertCount(5, $response['data']['channels']); $this->assertContains('console', $response['data']['channels']); + $this->assertContains("projects.{$this->getProject()['$id']}", $response['data']['channels']); $this->assertContains('executions', $response['data']['channels']); $this->assertContains("executions.{$executionId}", $response['data']['channels']); $this->assertContains("functions.{$functionId}", $response['data']['channels']); @@ -1362,8 +1363,9 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $responseUpdate['type']); $this->assertNotEmpty($responseUpdate['data']); $this->assertArrayHasKey('timestamp', $responseUpdate['data']); - $this->assertCount(4, $responseUpdate['data']['channels']); + $this->assertCount(5, $responseUpdate['data']['channels']); $this->assertContains('console', $responseUpdate['data']['channels']); + $this->assertContains("projects.{$this->getProject()['$id']}", $response['data']['channels']); $this->assertContains('executions', $responseUpdate['data']['channels']); $this->assertContains("executions.{$executionId}", $responseUpdate['data']['channels']); $this->assertContains("functions.{$functionId}", $responseUpdate['data']['channels']); From e6d8221c898fa1d00c5b1e246589c5190c044c7c Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 30 Sep 2024 17:08:17 +0200 Subject: [PATCH 205/241] tests(realtime): fix argument types --- tests/e2e/Services/Realtime/RealtimeBase.php | 67 ++++++++++++-------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 30c411ba93..99f31134c0 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -7,25 +7,34 @@ use WebSocket\ConnectionException; trait RealtimeBase { - private function getWebsocket($channels = [], $headers = [], $projectId = null): WebSocketClient - { + private function getWebsocket( + array $channels = [], + array $headers = [], + string $projectId = null + ): WebSocketClient { if (is_null($projectId)) { $projectId = $this->getProject()['$id']; } - $headers = array_merge([ - 'Origin' => 'appwrite.test' - ], $headers); + $headers = array_merge( + [ + "Origin" => "appwrite.test", + ], + $headers + ); $query = [ - 'project' => $projectId, - 'channels' => $channels + "project" => $projectId, + "channels" => $channels, ]; - return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ - 'headers' => $headers, - 'timeout' => 30, - ]); + return new WebSocketClient( + "ws://appwrite-traefik/v1/realtime?" . http_build_query($query), + [ + "headers" => $headers, + "timeout" => 30, + ] + ); } public function testConnection(): void @@ -33,7 +42,7 @@ trait RealtimeBase /** * Test for SUCCESS */ - $client = $this->getWebsocket(['documents']); + $client = $this->getWebsocket(["documents"]); $this->assertNotEmpty($client->receive()); $client->close(); } @@ -43,11 +52,11 @@ trait RealtimeBase $client = $this->getWebsocket(); $payload = json_decode($client->receive(), true); - $this->assertArrayHasKey('type', $payload); - $this->assertArrayHasKey('data', $payload); - $this->assertEquals('error', $payload['type']); - $this->assertEquals(1008, $payload['data']['code']); - $this->assertEquals('Missing channels', $payload['data']['message']); + $this->assertArrayHasKey("type", $payload); + $this->assertArrayHasKey("data", $payload); + $this->assertEquals("error", $payload["type"]); + $this->assertEquals(1008, $payload["data"]["code"]); + $this->assertEquals("Missing channels", $payload["data"]["message"]); \usleep(250000); // 250ms $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); @@ -55,18 +64,24 @@ trait RealtimeBase public function testConnectionFailureUnknownProject(): void { - $client = new WebSocketClient('ws://appwrite-traefik/v1/realtime?project=123', [ - 'headers' => [ - 'Origin' => 'appwrite.test' + $client = new WebSocketClient( + "ws://appwrite-traefik/v1/realtime?project=123", + [ + "headers" => [ + "Origin" => "appwrite.test", + ], ] - ]); + ); $payload = json_decode($client->receive(), true); - $this->assertArrayHasKey('type', $payload); - $this->assertArrayHasKey('data', $payload); - $this->assertEquals('error', $payload['type']); - $this->assertEquals(1008, $payload['data']['code']); - $this->assertEquals('Missing or unknown project ID', $payload['data']['message']); + $this->assertArrayHasKey("type", $payload); + $this->assertArrayHasKey("data", $payload); + $this->assertEquals("error", $payload["type"]); + $this->assertEquals(1008, $payload["data"]["code"]); + $this->assertEquals( + "Missing or unknown project ID", + $payload["data"]["message"] + ); \usleep(250000); // 250ms $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); From 51152ae87958ebad59facf09c7295e39537bbce4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 30 Sep 2024 23:07:37 +0300 Subject: [PATCH 206/241] Ignore junction tables --- src/Appwrite/Platform/Workers/Deletes.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 8b835c53f3..62fd8cd177 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -504,7 +504,18 @@ class Deletes extends Action foreach ($collections as $collection) { if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) { - $dbForProject->deleteCollection($collection->getId()); + try { + $dbForProject->deleteCollection($collection->getId()); + } catch (Throwable $e) { + Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); + + /** + * Ignore junction tables; + */ + if (!preg_match('/^_\d+_\d+$/', $collection->getId())) { + throw $e; + } + } } else { $this->deleteByGroup($collection->getId(), [], database: $dbForProject); } From 54f55f65f42f1f4731b3443f871a00f341869565 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 1 Oct 2024 17:17:36 +0300 Subject: [PATCH 207/241] deletes-worker-set-project --- app/controllers/api/projects.php | 1 + app/worker.php | 11 +++-------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index aafd480398..ab46e91330 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -923,6 +923,7 @@ App::delete('/v1/projects/:projectId') } $queueForDeletes + ->setProject($project) ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($project); diff --git a/app/worker.php b/app/worker.php index 9bcdae78e6..9f17987ba5 100644 --- a/app/worker.php +++ b/app/worker.php @@ -54,16 +54,11 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register) return $adapter; }, ['cache', 'register']); -Server::setResource('project', function (Message $message, Database $dbForConsole) { +Server::setResource('project', function (Message $message) { $payload = $message->getPayload() ?? []; - $project = new Document($payload['project'] ?? []); - if ($project->getId() === 'console') { - return $project; - } - - return $dbForConsole->getDocument('projects', $project->getId()); -}, ['message', 'dbForConsole']); + return new Document($payload['project'] ?? []); +}, ['message']); Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) { if ($project->isEmpty() || $project->getId() === 'console') { From d14edf4207f9721c64a1caae78ae8492629acf03 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:30:47 -0400 Subject: [PATCH 208/241] reverting: to set coroutines as head --- .github/workflows/codeql-phpstan.yml | 16 + app/cli.php | 256 +-- app/config/variables.php | 2 +- app/controllers/api/account.php | 581 +++--- app/controllers/api/avatars.php | 102 +- app/controllers/api/console.php | 12 +- app/controllers/api/databases.php | 427 ++-- app/controllers/api/functions.php | 211 +- app/controllers/api/graphql.php | 29 +- app/controllers/api/health.php | 130 +- app/controllers/api/locale.php | 20 +- app/controllers/api/messaging.php | 181 +- app/controllers/api/migrations.php | 54 +- app/controllers/api/project.php | 35 +- app/controllers/api/projects.php | 274 +-- app/controllers/api/proxy.php | 20 +- app/controllers/api/storage.php | 211 +- app/controllers/api/teams.php | 106 +- app/controllers/api/users.php | 111 +- app/controllers/api/vcs.php | 89 +- app/controllers/general.php | 263 ++- app/controllers/mock.php | 34 +- app/controllers/shared/api.php | 118 +- app/controllers/shared/api/auth.php | 22 +- app/controllers/web/console.php | 6 +- app/controllers/web/home.php | 4 +- app/http.php | 515 ++--- app/init.php | 1832 ++--------------- app/init/config.php | 37 + app/init/constants.php | 194 ++ app/init/database/filters.php | 397 ++++ app/init/database/formats.php | 43 + app/init/locale.php | 23 + app/init/resources.php | 1046 ++++++++++ app/realtime.php | 277 +-- app/views/install/compose.phtml | 5 +- app/worker.php | 363 +--- composer.json | 45 +- composer.lock | 622 +++--- docker-compose.yml | 6 +- docs/tutorials/add-route.md | 30 +- phpstan.neon | 11 + src/Appwrite/Auth/Auth.php | 36 +- src/Appwrite/Auth/Authentication.php | 57 + src/Appwrite/Auth/Validator/MockNumber.php | 5 +- src/Appwrite/Auth/Validator/Password.php | 2 +- src/Appwrite/Auth/Validator/Phone.php | 2 +- src/Appwrite/Event/Func.php | 4 +- src/Appwrite/Event/Validator/Event.php | 2 +- src/Appwrite/Functions/Validator/Headers.php | 2 +- src/Appwrite/Functions/Validator/Payload.php | 2 +- .../Validator/RuntimeSpecification.php | 2 +- src/Appwrite/GraphQL/Resolvers.php | 170 +- src/Appwrite/GraphQL/Schema.php | 119 +- src/Appwrite/GraphQL/Types/Mapper.php | 64 +- src/Appwrite/Migration/Migration.php | 36 +- src/Appwrite/Network/Validator/CNAME.php | 2 +- src/Appwrite/Network/Validator/Email.php | 4 +- src/Appwrite/Network/Validator/Origin.php | 4 +- src/Appwrite/Platform/Tasks/Doctor.php | 80 +- src/Appwrite/Platform/Tasks/Install.php | 11 +- src/Appwrite/Platform/Tasks/Migrate.php | 22 +- src/Appwrite/Platform/Tasks/QueueCount.php | 4 +- src/Appwrite/Platform/Tasks/QueueRetry.php | 4 +- src/Appwrite/Platform/Tasks/SSL.php | 4 +- src/Appwrite/Platform/Tasks/ScheduleBase.php | 123 +- .../Platform/Tasks/ScheduleExecutions.php | 30 +- .../Platform/Tasks/ScheduleFunctions.php | 17 +- .../Platform/Tasks/ScheduleMessages.php | 24 +- src/Appwrite/Platform/Tasks/Specs.php | 50 +- src/Appwrite/Platform/Tasks/Upgrade.php | 4 +- src/Appwrite/Platform/Workers/Audits.php | 6 +- src/Appwrite/Platform/Workers/Builds.php | 66 +- .../Platform/Workers/Certificates.php | 30 +- src/Appwrite/Platform/Workers/Deletes.php | 14 +- src/Appwrite/Platform/Workers/Mails.php | 6 +- src/Appwrite/Promises/Promise.php | 2 +- src/Appwrite/Promises/Swoole.php | 17 +- src/Appwrite/Specification/Format.php | 10 +- .../Specification/Format/OpenAPI3.php | 53 +- .../Specification/Format/Swagger2.php | 44 +- src/Appwrite/Task/Validator/Cron.php | 2 +- .../Utopia/Database/Validator/CompoundUID.php | 2 +- .../Utopia/Database/Validator/ProjectId.php | 2 +- src/Appwrite/Utopia/Queue/Connections.php | 36 + src/Appwrite/Utopia/Request.php | 36 +- src/Appwrite/Utopia/Response.php | 318 +-- src/Appwrite/Utopia/Response/Models.php | 304 +++ src/Appwrite/Utopia/View.php | 2 +- .../e2e/Services/Databases/DatabasesBase.php | 4 +- .../Databases/DatabasesCustomClientTest.php | 4 +- .../DatabasesPermissionsGuestTest.php | 20 +- .../e2e/Services/Functions/FunctionsBase.php | 5 +- .../Functions/FunctionsCustomServerTest.php | 6 +- tests/e2e/Services/GraphQL/Base.php | 2 +- .../Services/GraphQL/StorageClientTest.php | 1 - .../Services/GraphQL/StorageServerTest.php | 1 - .../Projects/ProjectsConsoleClientTest.php | 2 +- .../Realtime/RealtimeCustomClientTest.php | 5 +- .../Storage/StorageCustomClientTest.php | 4 +- .../Webhooks/WebhooksCustomServerTest.php | 5 +- tests/resources/docker/docker-compose.yml | 2 +- tests/unit/Auth/AuthTest.php | 35 +- tests/unit/Event/EventTest.php | 10 +- tests/unit/GraphQL/BuilderTest.php | 9 +- .../unit/Messaging/MessagingChannelsTest.php | 9 +- tests/unit/Migration/MigrationTest.php | 2 +- tests/unit/Utopia/RequestTest.php | 7 +- tests/unit/Utopia/ResponseTest.php | 10 +- 109 files changed, 5667 insertions(+), 5072 deletions(-) create mode 100644 .github/workflows/codeql-phpstan.yml create mode 100644 app/init/config.php create mode 100644 app/init/constants.php create mode 100644 app/init/database/filters.php create mode 100644 app/init/database/formats.php create mode 100644 app/init/locale.php create mode 100644 app/init/resources.php create mode 100644 phpstan.neon create mode 100644 src/Appwrite/Auth/Authentication.php create mode 100644 src/Appwrite/Utopia/Queue/Connections.php create mode 100644 src/Appwrite/Utopia/Response/Models.php diff --git a/.github/workflows/codeql-phpstan.yml b/.github/workflows/codeql-phpstan.yml new file mode 100644 index 0000000000..3253e2c38b --- /dev/null +++ b/.github/workflows/codeql-phpstan.yml @@ -0,0 +1,16 @@ +name: "CodeQL" + +on: [pull_request] +jobs: + lint: + name: CodeQL + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Run CodeQL + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/app/cli.php b/app/cli.php index 23502ec402..51a8e86137 100644 --- a/app/cli.php +++ b/app/cli.php @@ -7,195 +7,101 @@ use Appwrite\Event\Delete; use Appwrite\Event\Func; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; -use Utopia\CLI\CLI; +use Swoole\Runtime; +use Utopia\CLI\Adapters\Swoole as SwooleCLI; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Database\Database; -use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Dependency; use Utopia\Logger\Log; use Utopia\Platform\Service; -use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Registry\Registry; use Utopia\System\System; +global $registry, $container; + +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + // overwriting runtimes to be architectur agnostic for CLI Config::setParam('runtimes', (new Runtimes('v4'))->getAll(supported: false)); // require controllers after overwriting runtimes require_once __DIR__ . '/controllers/general.php'; -Authorization::disable(); +/** + * @var Registry $registry + * @var Container $container + */ +$context = new Dependency(); +$register = new Dependency(); +$logError = new Dependency(); +$queueForDeletes = new Dependency(); +$queueForFunctions = new Dependency(); +$queueForCertificates = new Dependency(); -CLI::setResource('register', fn () => $register); +$context + ->setName('context') + ->setCallback(fn () => $container); -CLI::setResource('cache', function ($pools) { - $list = Config::getParam('pools-cache', []); - $adapters = []; +$register + ->setName('register') + ->setCallback(function () use (&$registry): Registry { + return $registry; + }); - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } +$queueForFunctions + ->setName('queueForFunctions') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Func($queue); + }); - return new Cache(new Sharding($adapters)); -}, ['pools']); -CLI::setResource('pools', function (Registry $register) { - return $register->get('pools'); -}, ['register']); +$queueForDeletes + ->setName('queueForDeletes') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Delete($queue); + }); -CLI::setResource('dbForConsole', function ($pools, $cache) { - $sleep = 3; - $maxAttempts = 5; - $attempts = 0; - $ready = false; +$queueForCertificates + ->setName('queueForCertificates') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Certificate($queue); + }); - do { - $attempts++; - try { - // Prepare database connection - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); +$logError + ->setName('logError') + ->inject('register') + ->setCallback(function (Registry $register) { + return function (Throwable $error, string $namespace, string $action) use ($register) { + $logger = $register->get('logger'); - $dbForConsole = new Database($dbAdapter, $cache); + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $dbForConsole - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console'); + $log = new Log(); + $log->setNamespace($namespace); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); - // Ensure tables exist - $collections = Config::getParam('collections', [])['console']; - $last = \array_key_last($collections); + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', get_class($error)); - if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */ - throw new Exception('Tables not ready yet.'); - } + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('trace', $error->getTraceAsString()); - $ready = true; - } catch (\Throwable $err) { - Console::warning($err->getMessage()); - $pools->get('console')->reclaim(); - sleep($sleep); - } - } while ($attempts < $maxAttempts && !$ready); + $log->setAction($action); - if (!$ready) { - throw new Exception("Console is not ready yet. Please try again later."); - } + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - return $dbForConsole; -}, ['pools', 'cache']); - -CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $databases[$dsn->getHost()] = $database; - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); - - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -CLI::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); -CLI::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); -CLI::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); -CLI::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); -CLI::setResource('logError', function (Registry $register) { - return function (Throwable $error, string $namespace, string $action) use ($register) { - $logger = $register->get('logger'); - - if ($logger) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $log = new Log(); - $log->setNamespace($namespace); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', get_class($error)); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - - $log->setAction($action); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); try { $responseCode = $logger->addLog($log); @@ -203,18 +109,32 @@ CLI::setResource('logError', function (Registry $register) { } catch (Throwable $th) { Console::error('Error pushing log: ' . $th->getMessage()); } - } + } - Console::warning("Failed: {$error->getMessage()}"); - Console::warning($error->getTraceAsString()); - }; -}, ['register']); + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + }; + }); + +$container->set($context); +$container->set($logError); +$container->set($register); +$container->set($queueForDeletes); +$container->set($queueForFunctions); +$container->set($queueForCertificates); $platform = new Appwrite(); -$platform->init(Service::TYPE_TASK); +$platform->init(Service::TYPE_TASK, ['adapter' => new SwooleCLI(1)]); $cli = $platform->getCli(); +$cli + ->init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->disable(); + }); + $cli ->error() ->inject('error') @@ -222,4 +142,6 @@ $cli Console::error($error->getMessage()); }); -$cli->run(); +$cli + ->setContainer($container) + ->run(); diff --git a/app/config/variables.php b/app/config/variables.php index 113fbae335..161951e3de 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -254,7 +254,7 @@ return [ 'name' => '_APP_WORKER_PER_CORE', 'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.', 'introduction' => '0.13.0', - 'default' => 6, + 'default' => 2, 'required' => false, 'question' => '', 'filter' => '' diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 64441fee5c..a76b9dbea5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use Appwrite\Auth\MFA\Challenge; use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; @@ -28,7 +29,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Identities; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit as EventAudit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -45,15 +45,16 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Host; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; @@ -144,14 +145,13 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc ->trigger(); }; - -$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails) { - $roles = Authorization::getRoles(); +$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Authorization $authorization, Authentication $authentication) { + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); /** @var Utopia\Database\Document $user */ - $userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($userFromRequest->isEmpty()) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -197,7 +197,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session ->setAttribute('$permissions', [ @@ -206,7 +206,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res Permission::delete(Role::user($user->getId())), ])); - Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); + $authorization->skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); $dbForProject->purgeCachedDocument('users', $user->getId()); // Magic URL + Email OTP @@ -247,15 +247,15 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setParam('sessionId', $session->getId()); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $sessionSecret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $protocol = $request->getProtocol(); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -270,7 +270,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $response->dynamic($session, Response::MODEL_SESSION); }; -App::post('/v1/account') +Http::post('/v1/account') ->desc('Create account') ->groups(['api', 'account', 'auth']) ->label('event', 'users.[userId].create') @@ -298,7 +298,8 @@ App::post('/v1/account') ->inject('dbForProject') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -376,9 +377,9 @@ App::post('/v1/account') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -404,9 +405,9 @@ App::post('/v1/account') throw new Exception(Exception::USER_ALREADY_EXISTS); } - Authorization::unsetRole(Role::guests()->toString()); - Authorization::setRole(Role::user($user->getId())->toString()); - Authorization::setRole(Role::users()->toString()); + $authorization->removeRole(Role::guests()->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); $queueForEvents->setParam('userId', $user->getId()); @@ -415,7 +416,7 @@ App::post('/v1/account') ->dynamic($user, Response::MODEL_ACCOUNT); }); -App::get('/v1/account') +Http::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) ->label('scope', 'account') @@ -438,7 +439,7 @@ App::get('/v1/account') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::delete('/v1/account') +Http::delete('/v1/account') ->desc('Delete account') ->groups(['api', 'account']) ->label('event', 'users.[userId].delete') @@ -486,7 +487,7 @@ App::delete('/v1/account') $response->noContent(); }); -App::get('/v1/account/sessions') +Http::get('/v1/account/sessions') ->desc('List sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -501,15 +502,16 @@ App::get('/v1/account/sessions') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (Response $response, Document $user, Locale $locale, Document $project) { + ->inject('authorization') + ->inject('authentication') + ->action(function (Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = Auth::sessionVerify($sessions, $authentication->getSecret()); foreach ($sessions as $key => $session) {/** @var Document $session */ $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -527,7 +529,7 @@ App::get('/v1/account/sessions') ]), Response::MODEL_SESSION_LIST); }); -App::delete('/v1/account/sessions') +Http::delete('/v1/account/sessions') ->desc('Delete sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -548,7 +550,8 @@ App::delete('/v1/account/sessions') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes) { + ->inject('authentication') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -564,13 +567,13 @@ App::delete('/v1/account/sessions') ->setAttribute('current', false) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { $session->setAttribute('current', true); // If current session delete the cookies too $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $queueForEvents @@ -592,7 +595,7 @@ App::delete('/v1/account/sessions') $response->noContent(); }); -App::get('/v1/account/sessions/:sessionId') +Http::get('/v1/account/sessions/:sessionId') ->desc('Get session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -609,16 +612,17 @@ App::get('/v1/account/sessions/:sessionId') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Document $project) { + ->inject('authorization') + ->inject('authentication') + ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; foreach ($sessions as $session) {/** @var Document $session */ @@ -626,7 +630,7 @@ App::get('/v1/account/sessions/:sessionId') $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); $session - ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) + ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash($authentication->getSecret()))) ->setAttribute('countryName', $countryName) ->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '') ; @@ -638,7 +642,7 @@ App::get('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -App::delete('/v1/account/sessions/:sessionId') +Http::delete('/v1/account/sessions/:sessionId') ->desc('Delete session') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') @@ -661,12 +665,12 @@ App::delete('/v1/account/sessions/:sessionId') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->inject('project') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Document $project) { + ->inject('authentication') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -685,7 +689,7 @@ App::delete('/v1/account/sessions/:sessionId') $session->setAttribute('current', false); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $session ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); @@ -695,8 +699,8 @@ App::delete('/v1/account/sessions/:sessionId') } $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -718,7 +722,7 @@ App::delete('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -App::patch('/v1/account/sessions/:sessionId') +Http::patch('/v1/account/sessions/:sessionId') ->desc('Update session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -740,10 +744,11 @@ App::patch('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) { + ->inject('authentication') + ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Authentication $authentication) { $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -794,7 +799,7 @@ App::patch('/v1/account/sessions/:sessionId') return $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/email') +Http::post('/v1/account/sessions/email') ->alias('/v1/account/sessions') ->desc('Create email password session') ->groups(['api', 'account', 'auth', 'session']) @@ -825,7 +830,9 @@ App::post('/v1/account/sessions/email') ->inject('queueForEvents') ->inject('queueForMails') ->inject('hooks') - ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Authorization $authorization, Authentication $authentication) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -841,7 +848,7 @@ App::post('/v1/account/sessions/email') throw new Exception(Exception::USER_BLOCKED); // User is in status blocked } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -872,7 +879,7 @@ App::post('/v1/account/sessions/email') $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); // Re-hash if not using recommended algo if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) { @@ -893,15 +900,15 @@ App::post('/v1/account/sessions/email') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) ; } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -929,7 +936,7 @@ App::post('/v1/account/sessions/email') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/anonymous') +Http::post('/v1/account/sessions/anonymous') ->desc('Create anonymous session') ->groups(['api', 'account', 'auth', 'session']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -955,9 +962,11 @@ App::post('/v1/account/sessions/anonymous') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents) { + ->inject('authorization') + ->inject('authentication') + ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { $protocol = $request->getProtocol(); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -1003,7 +1012,7 @@ App::post('/v1/account/sessions/anonymous') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); // Create session token $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; @@ -1029,7 +1038,7 @@ App::post('/v1/account/sessions/anonymous') $detector->getDevice() )); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [ Permission::read(Role::user($user->getId())), @@ -1045,14 +1054,14 @@ App::post('/v1/account/sessions/anonymous') ; if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1067,7 +1076,7 @@ App::post('/v1/account/sessions/anonymous') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/sessions/token') +Http::post('/v1/account/sessions/token') ->desc('Create session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -1095,9 +1104,11 @@ App::post('/v1/account/sessions/token') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::get('/v1/account/sessions/oauth2/:provider') +Http::get('/v1/account/sessions/oauth2/:provider') ->desc('Create OAuth2 session') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1167,7 +1178,7 @@ App::get('/v1/account/sessions/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') +Http::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1197,7 +1208,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') +Http::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1228,7 +1239,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -App::get('/v1/account/sessions/oauth2/:provider/redirect') +Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 redirect') ->groups(['api', 'account', 'session']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1252,7 +1263,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) use ($oauthDefaultSuccess) { $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; @@ -1389,7 +1402,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = Auth::sessionVerify($sessions, $authentication->getSecret()); if ($current) { // Delete current session of new one. $currentDocument = $dbForProject->getDocument('sessions', $current); @@ -1486,15 +1499,16 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), ], - 'userId' => $userDoc->getId(), - 'userInternalId' => $userDoc->getInternalId(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), 'providerType' => MESSAGE_TYPE_EMAIL, 'identifier' => $email, ])); @@ -1504,8 +1518,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } } - Authorization::setRole(Role::user($user->getId())->toString()); - Authorization::setRole(Role::users()->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); if (false === $user->getAttribute('status')) { // Account is blocked $failureRedirect(Exception::USER_BLOCKED); // User is in status blocked @@ -1564,7 +1578,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->updateDocument('users', $user->getId(), $user); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); @@ -1586,7 +1600,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -1636,7 +1650,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $session->setAttribute('expire', $expire); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); } $queueForEvents @@ -1649,13 +1663,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); $query['domain'] = Config::getParam('cookieDomain'); - $query['key'] = Auth::$cookieName; + $query['key'] = $authentication->getCookieName(); $query['secret'] = Auth::encodeSession($user->getId(), $secret); } $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } if (isset($sessionUpgrade) && $sessionUpgrade) { @@ -1686,7 +1700,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); -App::get('/v1/account/tokens/oauth2/:provider') +Http::get('/v1/account/tokens/oauth2/:provider') ->desc('Create OAuth2 token') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1755,7 +1769,7 @@ App::get('/v1/account/tokens/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -App::post('/v1/account/tokens/magic-url') +Http::post('/v1/account/tokens/magic-url') ->alias('/v1/account/sessions/magic-url') ->desc('Create magic URL token') ->groups(['api', 'account', 'auth']) @@ -1785,7 +1799,8 @@ App::post('/v1/account/tokens/magic-url') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -1795,7 +1810,7 @@ App::post('/v1/account/tokens/magic-url') $phrase = Phrase::generate(); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -1850,7 +1865,7 @@ App::post('/v1/account/tokens/magic-url') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_MAGIC_URL); @@ -1867,7 +1882,7 @@ App::post('/v1/account/tokens/magic-url') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -1999,7 +2014,7 @@ App::post('/v1/account/tokens/magic-url') ->dynamic($token, Response::MODEL_TOKEN); }); -App::post('/v1/account/tokens/email') +Http::post('/v1/account/tokens/email') ->desc('Create email token (OTP)') ->groups(['api', 'account', 'auth']) ->label('scope', 'sessions.write') @@ -2027,7 +2042,8 @@ App::post('/v1/account/tokens/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2036,7 +2052,7 @@ App::post('/v1/account/tokens/email') $phrase = Phrase::generate(); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2089,7 +2105,7 @@ App::post('/v1/account/tokens/email') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::codeGenerator(6); @@ -2106,7 +2122,7 @@ App::post('/v1/account/tokens/email') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2228,7 +2244,7 @@ App::post('/v1/account/tokens/email') ->dynamic($token, Response::MODEL_TOKEN); }); -App::put('/v1/account/sessions/magic-url') +Http::put('/v1/account/sessions/magic-url') ->desc('Update magic URL session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2257,9 +2273,11 @@ App::put('/v1/account/sessions/magic-url') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::put('/v1/account/sessions/phone') +Http::put('/v1/account/sessions/phone') ->desc('Update phone session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2288,9 +2306,11 @@ App::put('/v1/account/sessions/phone') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('authorization') + ->inject('authentication') ->action($createSession); -App::post('/v1/account/tokens/phone') +Http::post('/v1/account/tokens/phone') ->alias('/v1/account/sessions/phone') ->desc('Create phone token') ->groups(['api', 'account']) @@ -2318,12 +2338,13 @@ App::post('/v1/account/tokens/phone') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('locale') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) { + ->inject('authorization') + ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2367,9 +2388,9 @@ App::post('/v1/account/tokens/phone') ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -2415,7 +2436,7 @@ App::post('/v1/account/tokens/phone') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2471,7 +2492,7 @@ App::post('/v1/account/tokens/phone') ->dynamic($token, Response::MODEL_TOKEN); }); -App::post('/v1/account/jwts') +Http::post('/v1/account/jwts') ->alias('/v1/account/jwt') ->desc('Create JWT') ->groups(['api', 'account', 'auth']) @@ -2489,14 +2510,15 @@ App::post('/v1/account/jwts') ->inject('response') ->inject('user') ->inject('dbForProject') - ->action(function (Response $response, Document $user, Database $dbForProject) { + ->inject('authentication') + ->action(function (Response $response, Document $user, Database $dbForProject, Authentication $authentication) { $sessions = $user->getAttribute('sessions', []); $current = new Document(); foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $current = $session; } } @@ -2515,7 +2537,7 @@ App::post('/v1/account/jwts') ])]), Response::MODEL_JWT); }); -App::get('/v1/account/prefs') +Http::get('/v1/account/prefs') ->desc('Get account preferences') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2537,7 +2559,7 @@ App::get('/v1/account/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::get('/v1/account/logs') +Http::get('/v1/account/logs') ->desc('List logs') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2554,7 +2576,8 @@ App::get('/v1/account/logs') ->inject('locale') ->inject('geodb') ->inject('dbForProject') - ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) { + ->inject('authorization') + ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Authorization $authorization) { try { $queries = Query::parseQueries($queries); @@ -2566,7 +2589,7 @@ App::get('/v1/account/logs') $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; $offset = $grouped['offset'] ?? 0; - $audit = new EventAudit($dbForProject); + $audit = new EventAudit($dbForProject, $authorization); $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); @@ -2602,7 +2625,101 @@ App::get('/v1/account/logs') ]), Response::MODEL_LOG_LIST); }); -App::patch('/v1/account/name') +Http::patch('/v1/account/email') + ->desc('Update email') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.email') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updateEmail') + ->label('sdk.description', '/docs/references/account/update-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.offline.model', '/account') + ->label('sdk.offline.key', 'current') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->inject('requestTimestamp') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('project') + ->inject('hooks') + ->inject('authorization') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { + // passwordUpdate will be empty if the user has never set a password + $passwordUpdate = $user->getAttribute('passwordUpdate'); + + if ( + !empty($passwordUpdate) && + !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) + ) { // Double check user password + throw new Exception(Exception::USER_INVALID_CREDENTIALS); + } + + $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); + + $oldEmail = $user->getAttribute('email'); + + $email = \strtolower($email); + + // Makes sure this email is not already used in another identity + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ + Query::equal('providerEmail', [$email]), + Query::notEqual('userInternalId', $user->getInternalId()), + ]); + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $user + ->setAttribute('email', $email) + ->setAttribute('emailVerification', false) // After this user needs to confirm mail again + ; + + if (empty($passwordUpdate)) { + $user + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('passwordUpdate', DateTime::now()); + } + + $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ])); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + try { + $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + } + $dbForProject->purgeCachedDocument('users', $user->getId()); + } catch (Duplicate) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_ACCOUNT); + }); + + +Http::patch('/v1/account/name') ->desc('Update name') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.name') @@ -2635,7 +2752,7 @@ App::patch('/v1/account/name') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/password') +Http::patch('/v1/account/password') ->desc('Update password') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.password') @@ -2705,99 +2822,7 @@ App::patch('/v1/account/password') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/email') - ->desc('Update email') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.email') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'updateEmail') - ->label('sdk.description', '/docs/references/account/update-email.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) - ->label('sdk.offline.model', '/account') - ->label('sdk.offline.key', 'current') - ->param('email', '', new Email(), 'User email.') - ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') - ->inject('requestTimestamp') - ->inject('response') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('project') - ->inject('hooks') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { - // passwordUpdate will be empty if the user has never set a password - $passwordUpdate = $user->getAttribute('passwordUpdate'); - - if ( - !empty($passwordUpdate) && - !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) - ) { // Double check user password - throw new Exception(Exception::USER_INVALID_CREDENTIALS); - } - - $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - - $oldEmail = $user->getAttribute('email'); - - $email = \strtolower($email); - - // Makes sure this email is not already used in another identity - $identityWithMatchingEmail = $dbForProject->findOne('identities', [ - Query::equal('providerEmail', [$email]), - Query::notEqual('userInternalId', $user->getInternalId()), - ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $user - ->setAttribute('email', $email) - ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ; - - if (empty($passwordUpdate)) { - $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) - ->setAttribute('passwordUpdate', DateTime::now()); - } - - $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ - Query::equal('identifier', [$email]), - ])); - - if ($target instanceof Document && !$target->isEmpty()) { - throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); - } - - try { - $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); - /** - * @var Document $oldTarget - */ - $oldTarget = $user->find('identifier', $oldEmail, 'targets'); - - if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); - } - $dbForProject->purgeCachedDocument('users', $user->getId()); - } catch (Duplicate) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($user, Response::MODEL_ACCOUNT); - }); - -App::patch('/v1/account/phone') +Http::patch('/v1/account/phone') ->desc('Update phone') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.phone') @@ -2822,7 +2847,8 @@ App::patch('/v1/account/phone') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -2835,7 +2861,7 @@ App::patch('/v1/account/phone') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ])); @@ -2866,7 +2892,7 @@ App::patch('/v1/account/phone') $oldTarget = $user->find('identifier', $oldPhone, 'targets'); if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); + $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); } $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Duplicate $th) { @@ -2878,7 +2904,7 @@ App::patch('/v1/account/phone') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/prefs') +Http::patch('/v1/account/prefs') ->desc('Update preferences') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.prefs') @@ -2911,7 +2937,7 @@ App::patch('/v1/account/prefs') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::patch('/v1/account/status') +Http::patch('/v1/account/status') ->desc('Update status') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.status') @@ -2931,7 +2957,8 @@ App::patch('/v1/account/status') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authentication') + ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authentication $authentication) { $user->setAttribute('status', false); @@ -2947,14 +2974,14 @@ App::patch('/v1/account/status') $protocol = $request->getProtocol(); $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::post('/v1/account/recovery') +Http::post('/v1/account/recovery') ->desc('Create password recovery') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -2981,14 +3008,15 @@ App::post('/v1/account/recovery') ->inject('locale') ->inject('queueForMails') ->inject('queueForEvents') - ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } $url = htmlentities($url); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3022,7 +3050,7 @@ App::post('/v1/account/recovery') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $recovery = $dbForProject->createDocument('tokens', $recovery ->setAttribute('$permissions', [ @@ -3044,7 +3072,7 @@ App::post('/v1/account/recovery') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.recovery.hello")) ->setParam('{{footer}}', $locale->getText("emails.recovery.footer")) ->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks")) @@ -3134,7 +3162,7 @@ App::post('/v1/account/recovery') ->dynamic($recovery, Response::MODEL_TOKEN); }); -App::put('/v1/account/recovery') +Http::put('/v1/account/recovery') ->desc('Create password recovery (confirmation)') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -3160,7 +3188,8 @@ App::put('/v1/account/recovery') ->inject('project') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) { + ->inject('authorization') + ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { $profile = $dbForProject->getDocument('users', $userId); if ($profile->isEmpty()) { @@ -3174,7 +3203,7 @@ App::put('/v1/account/recovery') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); @@ -3219,7 +3248,7 @@ App::put('/v1/account/recovery') $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); -App::post('/v1/account/verification') +Http::post('/v1/account/verification') ->desc('Create email verification') ->groups(['api', 'account']) ->label('scope', 'account') @@ -3244,7 +3273,8 @@ App::post('/v1/account/verification') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('authorization') + ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3255,7 +3285,7 @@ App::post('/v1/account/verification') throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $verificationSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_VERIFICATION); @@ -3272,7 +3302,7 @@ App::post('/v1/account/verification') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3294,7 +3324,7 @@ App::post('/v1/account/verification') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.verification.hello")) ->setParam('{{footer}}', $locale->getText("emails.verification.footer")) ->setParam('{{thanks}}', $locale->getText("emails.verification.thanks")) @@ -3384,7 +3414,7 @@ App::post('/v1/account/verification') ->dynamic($verification, Response::MODEL_TOKEN); }); -App::put('/v1/account/verification') +Http::put('/v1/account/verification') ->desc('Create email verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3406,9 +3436,10 @@ App::put('/v1/account/verification') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3421,7 +3452,7 @@ App::put('/v1/account/verification') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); @@ -3443,7 +3474,7 @@ App::put('/v1/account/verification') $response->dynamic($verification, Response::MODEL_TOKEN); }); -App::post('/v1/account/verification/phone') +Http::post('/v1/account/verification/phone') ->desc('Create phone verification') ->groups(['api', 'account', 'auth']) ->label('scope', 'account') @@ -3468,7 +3499,8 @@ App::post('/v1/account/verification/phone') ->inject('queueForMessaging') ->inject('project') ->inject('locale') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) { + ->inject('authorization') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -3482,7 +3514,7 @@ App::post('/v1/account/verification/phone') throw new Exception(Exception::USER_PHONE_ALREADY_VERIFIED); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3511,7 +3543,7 @@ App::post('/v1/account/verification/phone') 'ip' => $request->getIP(), ]); - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3571,7 +3603,7 @@ App::post('/v1/account/verification/phone') ->dynamic($verification, Response::MODEL_TOKEN); }); -App::put('/v1/account/verification/phone') +Http::put('/v1/account/verification/phone') ->desc('Update phone verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3593,9 +3625,10 @@ App::put('/v1/account/verification/phone') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3607,7 +3640,7 @@ App::put('/v1/account/verification/phone') throw new Exception(Exception::USER_INVALID_TOKEN); } - Authorization::setRole(Role::user($profile->getId())->toString()); + $authorization->addRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); @@ -3629,7 +3662,7 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); -App::patch('/v1/account/mfa') +Http::patch('/v1/account/mfa') ->desc('Update MFA') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') @@ -3682,8 +3715,8 @@ App::patch('/v1/account/mfa') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::get('/v1/account/mfa/factors') - ->desc('List factors') +Http::get('/v1/account/mfa/factors') + ->desc('List Factors') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3714,8 +3747,8 @@ App::get('/v1/account/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -App::post('/v1/account/mfa/authenticators/:type') - ->desc('Create authenticator') +Http::post('/v1/account/mfa/authenticators/:type') + ->desc('Create Authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3786,8 +3819,8 @@ App::post('/v1/account/mfa/authenticators/:type') $response->dynamic($model, Response::MODEL_MFA_TYPE); }); -App::put('/v1/account/mfa/authenticators/:type') - ->desc('Verify authenticator') +Http::put('/v1/account/mfa/authenticators/:type') + ->desc('Verify Authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3851,8 +3884,8 @@ App::put('/v1/account/mfa/authenticators/:type') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -App::post('/v1/account/mfa/recovery-codes') - ->desc('Create MFA recovery codes') +Http::post('/v1/account/mfa/recovery-codes') + ->desc('Create MFA Recovery Codes') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3893,8 +3926,8 @@ App::post('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::patch('/v1/account/mfa/recovery-codes') - ->desc('Regenerate MFA recovery codes') +Http::patch('/v1/account/mfa/recovery-codes') + ->desc('Regenerate MFA Recovery Codes') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3934,8 +3967,8 @@ App::patch('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::get('/v1/account/mfa/recovery-codes') - ->desc('Get MFA recovery codes') +Http::get('/v1/account/mfa/recovery-codes') + ->desc('Get MFA Recovery Codes') ->groups(['api', 'account', 'mfaProtected']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3964,8 +3997,8 @@ App::get('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::delete('/v1/account/mfa/authenticators/:type') - ->desc('Delete authenticator') +Http::delete('/v1/account/mfa/authenticators/:type') + ->desc('Delete Authenticator') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].delete.mfa') ->label('scope', 'account') @@ -4002,8 +4035,8 @@ App::delete('/v1/account/mfa/authenticators/:type') $response->noContent(); }); -App::post('/v1/account/mfa/challenge') - ->desc('Create MFA challenge') +Http::post('/v1/account/mfa/challenge') + ->desc('Create MFA Challenge') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].challenges.[challengeId].create') @@ -4190,8 +4223,8 @@ App::post('/v1/account/mfa/challenge') $response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE); }); -App::put('/v1/account/mfa/challenge') - ->desc('Create MFA challenge (confirmation)') +Http::put('/v1/account/mfa/challenge') + ->desc('Create MFA Challenge (confirmation)') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -4277,7 +4310,7 @@ App::put('/v1/account/mfa/challenge') $response->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/account/targets/push') +Http::post('/v1/account/targets/push') ->desc('Create push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4298,12 +4331,14 @@ App::post('/v1/account/targets/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization, Authentication $authentication) { $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; - $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $provider = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); @@ -4314,7 +4349,7 @@ App::post('/v1/account/targets/push') $device = $detector->getDevice(); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()); $session = $dbForProject->getDocument('sessions', $sessionId); try { @@ -4350,7 +4385,7 @@ App::post('/v1/account/targets/push') ->dynamic($target, Response::MODEL_TARGET); }); -App::put('/v1/account/targets/:targetId/push') +Http::put('/v1/account/targets/:targetId/push') ->desc('Update push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4370,9 +4405,10 @@ App::put('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4405,7 +4441,7 @@ App::put('/v1/account/targets/:targetId/push') ->dynamic($target, Response::MODEL_TARGET); }); -App::delete('/v1/account/targets/:targetId/push') +Http::delete('/v1/account/targets/:targetId/push') ->desc('Delete push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4425,8 +4461,9 @@ App::delete('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + ->inject('authorization') + ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4451,8 +4488,8 @@ App::delete('/v1/account/targets/:targetId/push') $response->noContent(); }); -App::get('/v1/account/identities') - ->desc('List identities') +Http::get('/v1/account/identities') + ->desc('List Identities') ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -4507,7 +4544,7 @@ App::get('/v1/account/identities') ]), Response::MODEL_IDENTITY_LIST); }); -App::delete('/v1/account/identities/:identityId') +Http::delete('/v1/account/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'account']) ->label('scope', 'account') diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index fcff3e4179..54ba096c5d 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -5,7 +5,7 @@ use Appwrite\URL\URL as URLParse; use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; -use Utopia\App; +use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -14,15 +14,17 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Fetch\Client; +use Utopia\Http\Http; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\HexColor; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; +use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\HexColor; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; $avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) { @@ -61,9 +63,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, unset($image); }; -$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) { +$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger, Authorization $auth) { try { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); @@ -114,7 +116,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro ->setAttribute('providerRefreshToken', $refreshToken) ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); + $auth->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Throwable $err) { @@ -122,7 +124,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro do { $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); $gitHubSession = new Document(); @@ -154,11 +156,42 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro 'id' => $githubId ]; } catch (Exception $error) { + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace('console'); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', get_class($error)); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('detailedTrace', $error->getTrace()); + + $log->setAction('avatarsGetGitHub'); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + $responseCode = $logger->addLog($log); + Console::info('GitHub error log pushed with status code: ' . $responseCode); + } + + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + return []; } }; -App::get('/v1/avatars/credit-cards/:code') +Http::get('/v1/avatars/credit-cards/:code') ->desc('Get credit card icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -178,7 +211,7 @@ App::get('/v1/avatars/credit-cards/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/browsers/:code') +Http::get('/v1/avatars/browsers/:code') ->desc('Get browser icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -198,7 +231,7 @@ App::get('/v1/avatars/browsers/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/flags/:code') +Http::get('/v1/avatars/flags/:code') ->desc('Get country flag') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -218,7 +251,7 @@ App::get('/v1/avatars/flags/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response)); -App::get('/v1/avatars/image') +Http::get('/v1/avatars/image') ->desc('Get image from URL') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -281,7 +314,7 @@ App::get('/v1/avatars/image') unset($image); }); -App::get('/v1/avatars/favicon') +Http::get('/v1/avatars/favicon') ->desc('Get favicon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -426,7 +459,7 @@ App::get('/v1/avatars/favicon') unset($image); }); -App::get('/v1/avatars/qr') +Http::get('/v1/avatars/qr') ->desc('Get QR code') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -466,7 +499,7 @@ App::get('/v1/avatars/qr') ->send($image->output('png', 9)); }); -App::get('/v1/avatars/initials') +Http::get('/v1/avatars/initials') ->desc('Get user initials') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -549,8 +582,8 @@ App::get('/v1/avatars/initials') ->file($image->getImageBlob()); }); -App::get('/v1/cards/cloud') - ->desc('Get front Of Cloud Card') +Http::get('/v1/cards/cloud') + ->desc('Get Front Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -571,8 +604,9 @@ App::get('/v1/cards/cloud') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -583,7 +617,7 @@ App::get('/v1/cards/cloud') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; @@ -756,8 +790,8 @@ App::get('/v1/cards/cloud') ->file($baseImage->getImageBlob()); }); -App::get('/v1/cards/cloud-back') - ->desc('Get back Of Cloud Card') +Http::get('/v1/cards/cloud-back') + ->desc('Get Back Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -778,8 +812,9 @@ App::get('/v1/cards/cloud-back') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -789,7 +824,7 @@ App::get('/v1/cards/cloud-back') $userId = $user->getId(); $email = $user->getAttribute('email', ''); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubId = $gitHub['id'] ?? ''; $isHero = \array_key_exists($email, $heroes); @@ -834,8 +869,8 @@ App::get('/v1/cards/cloud-back') ->file($baseImage->getImageBlob()); }); -App::get('/v1/cards/cloud-og') - ->desc('Get OG image From Cloud Card') +Http::get('/v1/cards/cloud-og') + ->desc('Get OG Image From Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -856,8 +891,9 @@ App::get('/v1/cards/cloud-og') ->inject('contributors') ->inject('employees') ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('auth') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { + $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -872,7 +908,7 @@ App::get('/v1/cards/cloud-og') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index eeb823a3d3..2a393497ae 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -2,12 +2,12 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Document; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\Text; -App::init() +Http::init() ->groups(['console']) ->inject('project') ->action(function (Document $project) { @@ -17,7 +17,7 @@ App::init() }); -App::get('/v1/console/variables') +Http::get('/v1/console/variables') ->desc('Get variables') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -56,8 +56,8 @@ App::get('/v1/console/variables') $response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES); }); -App::post('/v1/console/assistant') - ->desc('Ask query') +Http::post('/v1/console/assistant') + ->desc('Ask Query') ->groups(['api', 'assistant']) ->label('scope', 'assistant.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index b76548b725..4bc074fb8f 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -16,7 +16,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Indexes; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -35,6 +34,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Key; @@ -44,18 +44,19 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\FloatValidator; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\IP; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\FloatValidator; -use Utopia\Validator\Integer; -use Utopia\Validator\IP; -use Utopia\Validator\JSON; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; /** * * Create attribute of varying type @@ -77,7 +78,7 @@ use Utopia\Validator\WhiteList; * @throws ConflictException * @throws Exception */ -function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document +function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document { $key = $attribute->getAttribute('key'); $type = $attribute->getAttribute('type', ''); @@ -91,7 +92,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -226,6 +227,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att } function updateAttribute( + Authorization $authorization, string $databaseId, string $collectionId, string $key, @@ -239,10 +241,10 @@ function updateAttribute( int|float $min = null, int|float $max = null, array $elements = null, - array $options = [], string $newKey = null, + array $options = [], ): Document { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -421,19 +423,19 @@ function updateAttribute( return $attribute; } -App::init() +Http::init() ->groups(['api', 'database']) ->inject('request') ->inject('dbForProject') ->action(function (Request $request, Database $dbForProject) { $timeout = \intval($request->getHeader('x-appwrite-timeout')); - if (!empty($timeout) && App::isDevelopment()) { + if (!empty($timeout) && Http::isDevelopment()) { $dbForProject->setTimeout($timeout); } }); -App::post('/v1/databases') +Http::post('/v1/databases') ->desc('Create database') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].create') @@ -511,7 +513,7 @@ App::post('/v1/databases') ->dynamic($database, Response::MODEL_DATABASE); }); -App::get('/v1/databases') +Http::get('/v1/databases') ->desc('List databases') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -564,7 +566,7 @@ App::get('/v1/databases') ]), Response::MODEL_DATABASE_LIST); }); -App::get('/v1/databases/:databaseId') +Http::get('/v1/databases/:databaseId') ->desc('Get database') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -589,7 +591,7 @@ App::get('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -App::get('/v1/databases/:databaseId/logs') +Http::get('/v1/databases/:databaseId/logs') ->desc('List database logs') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -606,7 +608,8 @@ App::get('/v1/databases/:databaseId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -680,7 +683,7 @@ App::get('/v1/databases/:databaseId/logs') }); -App::put('/v1/databases/:databaseId') +Http::put('/v1/databases/:databaseId') ->desc('Update database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -718,7 +721,7 @@ App::put('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -App::delete('/v1/databases/:databaseId') +Http::delete('/v1/databases/:databaseId') ->desc('Delete database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -766,7 +769,7 @@ App::delete('/v1/databases/:databaseId') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections') +Http::post('/v1/databases/:databaseId/collections') ->desc('Create collection') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].create') @@ -790,9 +793,10 @@ App::post('/v1/databases/:databaseId/collections') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -833,7 +837,7 @@ App::post('/v1/databases/:databaseId/collections') ->dynamic($collection, Response::MODEL_COLLECTION); }); -App::get('/v1/databases/:databaseId/collections') +Http::get('/v1/databases/:databaseId/collections') ->alias('/v1/database/collections', ['databaseId' => 'default']) ->desc('List collections') ->groups(['api', 'database']) @@ -851,9 +855,10 @@ App::get('/v1/databases/:databaseId/collections') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -896,7 +901,7 @@ App::get('/v1/databases/:databaseId/collections') ]), Response::MODEL_COLLECTION_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId') +Http::get('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Get collection') ->groups(['api', 'database']) @@ -913,9 +918,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -930,7 +936,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/logs') +Http::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->alias('/v1/database/collections/:collectionId/logs', ['databaseId' => 'default']) ->desc('List collection logs') ->groups(['api', 'database']) @@ -949,9 +955,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1030,7 +1037,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') }); -App::put('/v1/databases/:databaseId/collections/:collectionId') +Http::put('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Update collection') ->groups(['api', 'database', 'schema']) @@ -1055,9 +1062,10 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1093,7 +1101,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId') +Http::delete('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Delete collection') ->groups(['api', 'database', 'schema']) @@ -1114,9 +1122,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1148,7 +1157,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') ->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default']) ->desc('Create string attribute') ->groups(['api', 'database', 'schema']) @@ -1175,7 +1184,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within required size $validator = new Text($size, 0); @@ -1197,7 +1207,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response @@ -1205,7 +1215,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') ->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default']) ->desc('Create email attribute') ->groups(['api', 'database', 'schema']) @@ -1230,7 +1240,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1240,14 +1251,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default']) ->desc('Create enum attribute') ->groups(['api', 'database', 'schema']) @@ -1273,7 +1284,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { if (!is_null($default) && !in_array($default, $elements)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); } @@ -1287,14 +1299,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default']) ->desc('Create IP address attribute') ->groups(['api', 'database', 'schema']) @@ -1319,7 +1331,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1329,14 +1342,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_IP, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default']) ->desc('Create URL attribute') ->groups(['api', 'database', 'schema']) @@ -1361,7 +1374,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1371,14 +1385,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') ->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default']) ->desc('Create integer attribute') ->groups(['api', 'database', 'schema']) @@ -1405,7 +1419,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within range $min = (is_null($min)) ? PHP_INT_MIN : \intval($min); @@ -1435,7 +1450,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1449,7 +1464,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') ->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default']) ->desc('Create float attribute') ->groups(['api', 'database', 'schema']) @@ -1476,7 +1491,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { // Ensure attribute default is within range $min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min); @@ -1509,7 +1525,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1523,7 +1539,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') ->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default']) ->desc('Create boolean attribute') ->groups(['api', 'database', 'schema']) @@ -1548,7 +1564,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1557,14 +1574,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') ->alias('/v1/database/collections/:collectionId/attributes/datetime', ['databaseId' => 'default']) ->desc('Create datetime attribute') ->groups(['api', 'database']) @@ -1589,7 +1606,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { $filters[] = 'datetime'; @@ -1601,14 +1619,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') +Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') ->alias('/v1/database/collections/:collectionId/attributes/relationship', ['databaseId' => 'default']) ->desc('Create relationship attribute') ->groups(['api', 'database']) @@ -1635,6 +1653,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') + ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -1647,12 +1666,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati Response $response, Database $dbForProject, EventDatabase $queueForDatabase, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { $key ??= $relatedCollectionId; $twoWayKey ??= $collectionId; - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1722,7 +1742,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati $response, $dbForProject, $queueForDatabase, - $queueForEvents + $queueForEvents, + $authorization ); $options = $attribute->getAttribute('options', []); @@ -1736,7 +1757,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') +Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->alias('/v1/database/collections/:collectionId/attributes', ['databaseId' => 'default']) ->desc('List attributes') ->groups(['api', 'database']) @@ -1753,9 +1774,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Attributes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1789,7 +1811,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') if ($cursor) { $attributeId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ + $cursorDocument = $authorization->skip(fn () => $dbForProject->find('attributes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$attributeId]), @@ -1814,7 +1836,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ]), Response::MODEL_ATTRIBUTE_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Get attribute') ->groups(['api', 'database']) @@ -1841,9 +1863,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1889,7 +1912,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') $response->dynamic($attribute, $model); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') ->desc('Update string attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1912,9 +1935,11 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -1932,7 +1957,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') ->desc('Update email attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1954,8 +1979,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -1973,7 +2000,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') ->desc('Update enum attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1996,8 +2023,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2016,7 +2045,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') ->desc('Update IP address attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2038,8 +2067,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:k ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2057,7 +2088,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:k ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') ->desc('Update URL attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2079,8 +2110,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/: ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2098,7 +2131,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/: ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') ->desc('Update integer attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2122,8 +2155,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2149,7 +2184,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') ->desc('Update float attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2173,8 +2208,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2200,7 +2237,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') ->desc('Update boolean attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2222,8 +2259,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boole ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2240,7 +2279,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boole ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') ->desc('Update dateTime attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2262,8 +2301,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $attribute = updateAttribute( + authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2280,7 +2321,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') ->desc('Update relationship attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2301,6 +2342,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') + ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -2309,9 +2351,11 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ?string $newKey, Response $response, Database $dbForProject, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization ) { $attribute = updateAttribute( + $authorization, $databaseId, $collectionId, $key, @@ -2336,7 +2380,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Delete attribute') ->groups(['api', 'database', 'schema']) @@ -2358,9 +2402,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('queueForUsage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2449,7 +2494,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') +Http::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('Create index') ->groups(['api', 'database']) @@ -2474,9 +2519,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2619,7 +2665,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->dynamic($index, Response::MODEL_INDEX); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') +Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('List indexes') ->groups(['api', 'database']) @@ -2636,9 +2682,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->param('queries', [], new Indexes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Indexes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2668,7 +2715,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') if ($cursor) { $indexId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ + $cursorDocument = $authorization->skip(fn () => $dbForProject->find('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$indexId]), @@ -2689,7 +2736,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ]), Response::MODEL_INDEX_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Get index') ->groups(['api', 'database']) @@ -2706,9 +2753,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2728,7 +2776,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Delete index') ->groups(['api', 'database']) @@ -2749,9 +2797,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2792,7 +2841,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $response->noContent(); }); -App::post('/v1/databases/:databaseId/collections/:collectionId/documents') +Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('Create document') ->groups(['api', 'database']) @@ -2823,7 +2872,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2835,16 +2885,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead'); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -2882,8 +2932,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { - throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')'); + if (!$authorization->isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')'); } } } @@ -2894,17 +2944,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $data['$permissions'] = $permissions; $document = new Document($data); - $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, $authorization) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization($permission); - $valid = $validator->isValid($collection->getPermissionsByType($permission)); + $valid = $authorization->isValid(new Input($permission, $collection->getPermissionsByType($permission))); if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } if ($permission === Database::PERMISSION_UPDATE) { - $valid = $valid || $validator->isValid($document->getUpdate()); + $valid = $valid || $authorization->isValid($document->getUpdate()); if ($documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -2931,7 +2980,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -2945,7 +2994,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $relation = new Document($relation); } if ($relation instanceof Document) { - $current = Authorization::skip( + $current = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId()) ); @@ -2985,7 +3034,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3005,7 +3054,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3044,7 +3093,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('List documents') ->groups(['api', 'database']) @@ -3063,16 +3112,17 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3096,7 +3146,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') if ($cursor) { $documentId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$documentId}' for the 'cursor' value not found."); @@ -3115,7 +3165,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool { + $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization): bool { if ($document->isEmpty()) { return false; } @@ -3142,7 +3192,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); + $relatedCollection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); foreach ($relations as $index => $doc) { if ($doc instanceof Document) { @@ -3199,7 +3249,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ]), Response::MODEL_DOCUMENT_LIST); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Get document') ->groups(['api', 'database']) @@ -3220,17 +3270,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3250,7 +3301,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { if ($document->isEmpty()) { return; } @@ -3274,7 +3325,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3291,7 +3342,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen $response->dynamic($document, Response::MODEL_DOCUMENT); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') +Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) ->desc('List document logs') ->groups(['api', 'database']) @@ -3311,9 +3362,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3395,7 +3447,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ]), Response::MODEL_LOG_LIST); }); -App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Update document') ->groups(['api', 'database']) @@ -3425,7 +3477,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->inject('dbForProject') ->inject('queueForEvents') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) { + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Authorization $authorization) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -3433,16 +3486,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3450,7 +3503,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum // Read permission should not be required for update /** @var Document $document */ - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3464,7 +3517,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -3477,7 +3530,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -3492,7 +3545,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $data['$permissions'] = $permissions; $newDocument = new Document($data); - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) { + $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, $authorization) { $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP @@ -3514,7 +3567,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3529,7 +3582,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $relation = new Document($relation); } if ($relation instanceof Document) { - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( + $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument( 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId() )); @@ -3578,7 +3631,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3598,7 +3651,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3631,7 +3684,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->setPayload($response->getPayload(), sensitive: $relationships); }); -App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Delete document') ->groups(['api', 'database']) @@ -3660,24 +3713,25 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->inject('authorization') + ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for delete - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3691,7 +3745,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu }); // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3711,7 +3765,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = Authorization::skip( + $relatedCollection = $authorization->skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3751,7 +3805,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $response->noContent(); }); -App::get('/v1/databases/usage') +Http::get('/v1/databases/usage') ->desc('Get databases usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3764,7 +3818,8 @@ App::get('/v1/databases/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -3776,7 +3831,7 @@ App::get('/v1/databases/usage') METRIC_DATABASES_STORAGE ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3832,7 +3887,7 @@ App::get('/v1/databases/usage') ]), Response::MODEL_USAGE_DATABASES); }); -App::get('/v1/databases/:databaseId/usage') +Http::get('/v1/databases/:databaseId/usage') ->desc('Get database usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3846,7 +3901,8 @@ App::get('/v1/databases/:databaseId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -3863,7 +3919,7 @@ App::get('/v1/databases/:databaseId/usage') str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_STORAGE) ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3918,7 +3974,7 @@ App::get('/v1/databases/:databaseId/usage') ]), Response::MODEL_USAGE_DATABASE); }); -App::get('/v1/databases/:databaseId/collections/:collectionId/usage') +Http::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) ->desc('Get collection usage stats') ->groups(['api', 'database', 'usage']) @@ -3934,7 +3990,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject, Authorization $authorization) { $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); @@ -3951,7 +4008,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collectionDocument->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 9c3e6782b4..d0aa60ad06 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use Appwrite\Event\Build; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -20,11 +21,11 @@ use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Executions; use Appwrite\Utopia\Database\Validator\Queries\Functions; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Rule; use Executor\Executor; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -37,24 +38,25 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\AnyOf; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Storage\Device; use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; -use Utopia\Swoole\Request; use Utopia\System\System; -use Utopia\Validator\AnyOf; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -133,7 +135,7 @@ $redeployVcs = function (Request $request, Document $function, Document $project ->setTemplate($template); }; -App::post('/v1/functions') +Http::post('/v1/functions') ->groups(['api', 'functions']) ->desc('Create function') ->label('scope', 'functions.write') @@ -171,8 +173,8 @@ App::post('/v1/functions') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -183,7 +185,8 @@ App::post('/v1/functions') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { + ->inject('authorization') + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); @@ -247,7 +250,7 @@ App::post('/v1/functions') 'specification' => $specification ])); - $schedule = Authorization::skip( + $schedule = $authorization->skip( fn () => $dbForConsole->createDocument('schedules', new Document([ 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region 'resourceType' => 'function', @@ -329,7 +332,7 @@ App::post('/v1/functions') $routeSubdomain = ID::unique(); $domain = "{$routeSubdomain}.{$functionsDomain}"; - $rule = Authorization::skip( + $rule = $authorization->skip( fn () => $dbForConsole->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -396,7 +399,7 @@ App::post('/v1/functions') ->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions') +Http::get('/v1/functions') ->groups(['api', 'functions']) ->desc('List functions') ->label('scope', 'functions.read') @@ -450,7 +453,7 @@ App::get('/v1/functions') ]), Response::MODEL_FUNCTION_LIST); }); -App::get('/v1/functions/runtimes') +Http::get('/v1/functions/runtimes') ->groups(['api', 'functions']) ->desc('List runtimes') ->label('scope', 'functions.read') @@ -483,7 +486,7 @@ App::get('/v1/functions/runtimes') ]), Response::MODEL_RUNTIME_LIST); }); -App::get('/v1/functions/specifications') +Http::get('/v1/functions/specifications') ->groups(['api', 'functions']) ->desc('List available function runtime specifications') ->label('scope', 'functions.read') @@ -519,7 +522,7 @@ App::get('/v1/functions/specifications') ]), Response::MODEL_SPECIFICATION_LIST); }); -App::get('/v1/functions/:functionId') +Http::get('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Get function') ->label('scope', 'functions.read') @@ -543,7 +546,7 @@ App::get('/v1/functions/:functionId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions/:functionId/usage') +Http::get('/v1/functions/:functionId/usage') ->desc('Get function usage') ->groups(['api', 'functions', 'usage']) ->label('scope', 'functions.read') @@ -557,7 +560,8 @@ App::get('/v1/functions/:functionId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $functionId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -580,7 +584,7 @@ App::get('/v1/functions/:functionId/usage') str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -647,7 +651,7 @@ App::get('/v1/functions/:functionId/usage') ]), Response::MODEL_USAGE_FUNCTION); }); -App::get('/v1/functions/usage') +Http::get('/v1/functions/usage') ->desc('Get functions usage') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -660,7 +664,8 @@ App::get('/v1/functions/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -678,7 +683,7 @@ App::get('/v1/functions/usage') METRIC_EXECUTIONS_MB_SECONDS, ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -746,7 +751,7 @@ App::get('/v1/functions/usage') ]), Response::MODEL_USAGE_FUNCTIONS); }); -App::put('/v1/functions/:functionId') +Http::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update function') ->label('scope', 'functions.write') @@ -780,8 +785,8 @@ App::put('/v1/functions/:functionId') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -791,7 +796,8 @@ App::put('/v1/functions/:functionId') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { + ->inject('authorization') + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { // TODO: If only branch changes, re-deploy $function = $dbForProject->getDocument('functions', $functionId); @@ -894,7 +900,7 @@ App::put('/v1/functions/:functionId') // Enforce Cold Start if spec limits change. if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); } catch (\Throwable $th) { @@ -941,14 +947,14 @@ App::put('/v1/functions/:functionId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents->setParam('functionId', $function->getId()); $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::get('/v1/functions/:functionId/deployments/:deploymentId/download') +Http::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->groups(['api', 'functions']) ->desc('Download deployment') ->label('scope', 'functions.read') @@ -1033,7 +1039,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') } }); -App::patch('/v1/functions/:functionId/deployments/:deploymentId') +Http::patch('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Update deployment') ->label('scope', 'functions.write') @@ -1053,7 +1059,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForConsole') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -1086,7 +1093,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents ->setParam('functionId', $function->getId()) @@ -1095,7 +1102,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -App::delete('/v1/functions/:functionId') +Http::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete function') ->label('scope', 'functions.write') @@ -1114,7 +1121,8 @@ App::delete('/v1/functions/:functionId') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('dbForConsole') - ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -1131,7 +1139,7 @@ App::delete('/v1/functions/:functionId') $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) @@ -1142,7 +1150,7 @@ App::delete('/v1/functions/:functionId') $response->noContent(); }); -App::post('/v1/functions/:functionId/deployments') +Http::post('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('Create deployment') ->label('scope', 'functions.write') @@ -1361,7 +1369,7 @@ App::post('/v1/functions/:functionId/deployments') ->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -App::get('/v1/functions/:functionId/deployments') +Http::get('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('List deployments') ->label('scope', 'functions.read') @@ -1438,7 +1446,7 @@ App::get('/v1/functions/:functionId/deployments') ]), Response::MODEL_DEPLOYMENT_LIST); }); -App::get('/v1/functions/:functionId/deployments/:deploymentId') +Http::get('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Get deployment') ->label('scope', 'functions.read') @@ -1481,7 +1489,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -App::delete('/v1/functions/:functionId/deployments/:deploymentId') +Http::delete('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Delete deployment') ->label('scope', 'functions.write') @@ -1545,7 +1553,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId') $response->noContent(); }); -App::post('/v1/functions/:functionId/deployments/:deploymentId/build') +Http::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->groups(['api', 'functions']) ->desc('Rebuild deployment') @@ -1568,7 +1576,8 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1614,7 +1623,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build') $response->noContent(); }); -App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') +Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->groups(['api', 'functions']) ->desc('Cancel deployment') ->label('scope', 'functions.write') @@ -1632,7 +1641,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1645,7 +1655,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { $buildId = ID::unique(); @@ -1687,7 +1697,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist @@ -1703,7 +1713,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $response->dynamic($build, Response::MODEL_BUILD); }); -App::post('/v1/functions/:functionId/executions') +Http::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create execution') ->label('scope', 'execution.write') @@ -1733,7 +1743,9 @@ App::post('/v1/functions/:functionId/executions') ->inject('queueForUsage') ->inject('queueForFunctions') ->inject('geodb') - ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, Authorization $authorization, Authentication $authentication) { $async = \strval($async) === 'true' || \strval($async) === '1'; if (!$async && !is_null($scheduledAt)) { @@ -1763,10 +1775,10 @@ App::post('/v1/functions/:functionId/executions') throw new Exception($validator->getDescription(), 400); } - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -1782,7 +1794,7 @@ App::post('/v1/functions/:functionId/executions') throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -1793,7 +1805,7 @@ App::post('/v1/functions/:functionId/executions') } /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new Exception(Exception::BUILD_NOT_FOUND); } @@ -1802,10 +1814,8 @@ App::post('/v1/functions/:functionId/executions') throw new Exception(Exception::BUILD_NOT_READY); } - $validator = new Authorization('execute'); - - if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + if (!$authorization->isValid(new Input('execute', $function->getAttribute('execute')))) { // Check if user has write access to execute function + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } $jwt = ''; // initialize @@ -1815,7 +1825,7 @@ App::post('/v1/functions/:functionId/executions') foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too $current = $session; } } @@ -1899,8 +1909,9 @@ App::post('/v1/functions/:functionId/executions') ->setContext('function', $function); if ($async) { + if (is_null($scheduledAt)) { - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); $queueForFunctions ->setType('http') ->setExecution($execution) @@ -1941,7 +1952,7 @@ App::post('/v1/functions/:functionId/executions') ->setAttribute('scheduleInternalId', $schedule->getInternalId()) ->setAttribute('scheduledAt', $scheduledAt); - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response @@ -2027,8 +2038,7 @@ App::post('/v1/functions/:functionId/executions') runtimeEntrypoint: $command, cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 + logging: $function->getAttribute('logging', true) ); $headersFiltered = []; @@ -2069,10 +2079,10 @@ App::post('/v1/functions/:functionId/executions') ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT))) ; - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2105,7 +2115,7 @@ App::post('/v1/functions/:functionId/executions') ->dynamic($execution, Response::MODEL_EXECUTION); }); -App::get('/v1/functions/:functionId/executions') +Http::get('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('List executions') ->label('scope', 'execution.read') @@ -2122,11 +2132,12 @@ App::get('/v1/functions/:functionId/executions') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->inject('authorization') + ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2169,7 +2180,7 @@ App::get('/v1/functions/:functionId/executions') $results = $dbForProject->find('executions', $queries); $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2186,7 +2197,7 @@ App::get('/v1/functions/:functionId/executions') ]), Response::MODEL_EXECUTION_LIST); }); -App::get('/v1/functions/:functionId/executions/:executionId') +Http::get('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Get execution') ->label('scope', 'execution.read') @@ -2202,11 +2213,12 @@ App::get('/v1/functions/:functionId/executions/:executionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->inject('authorization') + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2222,7 +2234,7 @@ App::get('/v1/functions/:functionId/executions/:executionId') throw new Exception(Exception::EXECUTION_NOT_FOUND); } - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2233,7 +2245,7 @@ App::get('/v1/functions/:functionId/executions/:executionId') $response->dynamic($execution, Response::MODEL_EXECUTION); }); -App::delete('/v1/functions/:functionId/executions/:executionId') +Http::delete('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Delete execution') ->label('scope', 'execution.write') @@ -2252,7 +2264,8 @@ App::delete('/v1/functions/:functionId/executions/:executionId') ->inject('dbForProject') ->inject('dbForConsole') ->inject('queueForEvents') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2289,7 +2302,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } } @@ -2303,7 +2316,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId') // Variables -App::post('/v1/functions/:functionId/variables') +Http::post('/v1/functions/:functionId/variables') ->desc('Create variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2322,7 +2335,8 @@ App::post('/v1/functions/:functionId/variables') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2360,14 +2374,14 @@ App::post('/v1/functions/:functionId/variables') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); }); -App::get('/v1/functions/:functionId/variables') +Http::get('/v1/functions/:functionId/variables') ->desc('List variables') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2394,7 +2408,7 @@ App::get('/v1/functions/:functionId/variables') ]), Response::MODEL_VARIABLE_LIST); }); -App::get('/v1/functions/:functionId/variables/:variableId') +Http::get('/v1/functions/:functionId/variables/:variableId') ->desc('Get variable') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2433,7 +2447,7 @@ App::get('/v1/functions/:functionId/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::put('/v1/functions/:functionId/variables/:variableId') +Http::put('/v1/functions/:functionId/variables/:variableId') ->desc('Update variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2453,7 +2467,8 @@ App::put('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); @@ -2489,12 +2504,12 @@ App::put('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::delete('/v1/functions/:functionId/variables/:variableId') +Http::delete('/v1/functions/:functionId/variables/:variableId') ->desc('Delete variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2511,7 +2526,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) { + ->inject('authorization') + ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2537,12 +2553,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->noContent(); }); -App::get('/v1/functions/templates') +Http::get('/v1/functions/templates') ->groups(['api']) ->desc('List function templates') ->label('scope', 'public') @@ -2580,7 +2596,7 @@ App::get('/v1/functions/templates') ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); }); -App::get('/v1/functions/templates/:templateId') +Http::get('/v1/functions/templates/:templateId') ->desc('Get function template') ->label('scope', 'public') ->label('sdk.namespace', 'functions') @@ -2595,9 +2611,10 @@ App::get('/v1/functions/templates/:templateId') ->action(function (string $templateId, Response $response) { $templates = Config::getParam('function-templates', []); - $template = array_shift(\array_filter($templates, function ($template) use ($templateId) { + $array = \array_filter($templates, function ($template) use ($templateId) { return $template['id'] === $templateId; - })); + }); + $template = array_shift($array); if (empty($template)) { throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index f79f433b5c..0e7ddc783d 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -14,27 +14,28 @@ use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; use Swoole\Coroutine\WaitGroup; -use Utopia\App; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Http; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\JSON; -use Utopia\Validator\Text; -App::init() +Http::init() ->groups(['graphql']) ->inject('project') - ->action(function (Document $project) { + ->inject('authorization') + ->action(function (Document $project, Authorization $authorization) { if ( array_key_exists('graphql', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } }); -App::get('/v1/graphql') +Http::get('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -74,7 +75,7 @@ App::get('/v1/graphql') ->json($output); }); -App::post('/v1/graphql/mutation') +Http::post('/v1/graphql/mutation') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -119,7 +120,7 @@ App::post('/v1/graphql/mutation') ->json($output); }); -App::post('/v1/graphql') +Http::post('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -156,7 +157,6 @@ App::post('/v1/graphql') if (\str_starts_with($type, 'multipart/form-data')) { $query = parseMultipart($query, $request); } - $output = execute($schema, $promiseAdapter, $query); $response @@ -205,7 +205,7 @@ function execute( $validations[] = new QueryComplexity($maxComplexity); $validations[] = new QueryDepth($maxDepth); } - if (App::getMode() === App::MODE_TYPE_PRODUCTION) { + if (Http::getMode() === Http::MODE_TYPE_PRODUCTION) { $flags = DebugFlag::NONE; } @@ -306,9 +306,10 @@ function processResult($result, $debugFlags): array ); } -App::shutdown() +Http::shutdown() ->groups(['schema']) ->inject('project') - ->action(function (Document $project) { - Schema::setDirty($project->getId()); + ->inject('schemaVariable') + ->action(function (Document $project, Schema $schemaVariable) { + $schemaVariable->setDirty($project->getId()); }); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index f4581df8e4..47d80dbf71 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -3,12 +3,19 @@ use Appwrite\ClamAV\Network; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Database\Document; use Utopia\Domains\Validator\PublicDomain; -use Utopia\Pools\Group; +use Utopia\Http\Http; +use Utopia\Http\Validator\Domain; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Multiple; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Queue\Client; use Utopia\Queue\Connection; use Utopia\Registry\Registry; @@ -16,13 +23,8 @@ use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\System\System; -use Utopia\Validator\Domain; -use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::get('/v1/health') +Http::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -45,7 +47,7 @@ App::get('/v1/health') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/version') +Http::get('/v1/health/version') ->desc('Get version') ->groups(['api', 'health']) ->label('scope', 'public') @@ -57,7 +59,7 @@ App::get('/v1/health/version') $response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION); }); -App::get('/v1/health/db') +Http::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -70,21 +72,34 @@ App::get('/v1/health/db') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; $configs = [ - 'Console.DB' => Config::getParam('pools-console'), - 'Projects.DB' => Config::getParam('pools-database'), + 'console' => Config::getParam('pools-console'), + 'database' => Config::getParam('pools-database'), ]; foreach ($configs as $key => $config) { foreach ($config as $database) { + $checkStart = \microtime(true); + try { - $adapter = $pools->get($database)->pop()->getResource(); + + $pool = $pools['pools-'.$key.'-'.$database]['pool']; + $dsn = $pools['pools-'.$key.'-'.$database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($dsn->getPath()); - $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ @@ -111,7 +126,7 @@ App::get('/v1/health/db') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/cache') +Http::get('/v1/health/cache') ->desc('Get cache') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -124,7 +139,8 @@ App::get('/v1/health/cache') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -134,10 +150,15 @@ App::get('/v1/health/cache') foreach ($configs as $key => $config) { foreach ($config as $database) { + $checkStart = \microtime(true); try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-cache-' . $database]['pool']; + $dsn = $pools['pools-cache-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); - $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ @@ -158,6 +179,8 @@ App::get('/v1/health/cache') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -168,7 +191,7 @@ App::get('/v1/health/cache') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/queue') +Http::get('/v1/health/queue') ->desc('Get queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -181,7 +204,8 @@ App::get('/v1/health/queue') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -190,12 +214,16 @@ App::get('/v1/health/queue') ]; foreach ($configs as $key => $config) { + $checkStart = \microtime(true); + foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); - - $checkStart = \microtime(true); + $pool = $pools['pools-queue-' . $database]['pool']; + $dsn = $pools['pools-queue-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); if ($adapter->ping()) { $output[] = new Document([ 'name' => $key . " ($database)", @@ -215,6 +243,8 @@ App::get('/v1/health/queue') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -225,7 +255,7 @@ App::get('/v1/health/queue') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/pubsub') +Http::get('/v1/health/pubsub') ->desc('Get pubsub') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -238,7 +268,8 @@ App::get('/v1/health/pubsub') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->action(function (Response $response, Group $pools) { + ->inject('connections') + ->action(function (Response $response, array $pools, Connections $connections) { $output = []; @@ -249,7 +280,12 @@ App::get('/v1/health/pubsub') foreach ($configs as $key => $config) { foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-pubsub-' . $database]['pool']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Connection\Redis($connection); $checkStart = \microtime(true); @@ -272,6 +308,8 @@ App::get('/v1/health/pubsub') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); + } finally { + $connections->reclaim(); } } } @@ -282,7 +320,7 @@ App::get('/v1/health/pubsub') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -App::get('/v1/health/time') +Http::get('/v1/health/time') ->desc('Get time') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -339,7 +377,7 @@ App::get('/v1/health/time') $response->dynamic(new Document($output), Response::MODEL_HEALTH_TIME); }); -App::get('/v1/health/queue/webhooks') +Http::get('/v1/health/queue/webhooks') ->desc('Get webhooks queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -366,7 +404,7 @@ App::get('/v1/health/queue/webhooks') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/logs') +Http::get('/v1/health/queue/logs') ->desc('Get logs queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -393,7 +431,7 @@ App::get('/v1/health/queue/logs') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/certificate') +Http::get('/v1/health/certificate') ->desc('Get the SSL certificate for a domain') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -443,7 +481,7 @@ App::get('/v1/health/certificate') ]), Response::MODEL_HEALTH_CERTIFICATE); }, ['response']); -App::get('/v1/health/queue/certificates') +Http::get('/v1/health/queue/certificates') ->desc('Get certificates queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -470,7 +508,7 @@ App::get('/v1/health/queue/certificates') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/builds') +Http::get('/v1/health/queue/builds') ->desc('Get builds queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -497,7 +535,7 @@ App::get('/v1/health/queue/builds') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/databases') +Http::get('/v1/health/queue/databases') ->desc('Get databases queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -525,7 +563,7 @@ App::get('/v1/health/queue/databases') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/deletes') +Http::get('/v1/health/queue/deletes') ->desc('Get deletes queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -552,7 +590,7 @@ App::get('/v1/health/queue/deletes') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/mails') +Http::get('/v1/health/queue/mails') ->desc('Get mails queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -579,7 +617,7 @@ App::get('/v1/health/queue/mails') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/messaging') +Http::get('/v1/health/queue/messaging') ->desc('Get messaging queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -606,7 +644,7 @@ App::get('/v1/health/queue/messaging') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/migrations') +Http::get('/v1/health/queue/migrations') ->desc('Get migrations queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -633,7 +671,7 @@ App::get('/v1/health/queue/migrations') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/functions') +Http::get('/v1/health/queue/functions') ->desc('Get functions queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -660,7 +698,7 @@ App::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/usage') +Http::get('/v1/health/queue/usage') ->desc('Get usage queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -687,7 +725,7 @@ App::get('/v1/health/queue/usage') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/queue/usage-dump') +Http::get('/v1/health/queue/usage-dump') ->desc('Get usage dump queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -714,7 +752,7 @@ App::get('/v1/health/queue/usage-dump') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/storage/local') +Http::get('/v1/health/storage/local') ->desc('Get local storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -757,7 +795,7 @@ App::get('/v1/health/storage/local') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/storage') +Http::get('/v1/health/storage') ->desc('Get storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -798,7 +836,7 @@ App::get('/v1/health/storage') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -App::get('/v1/health/anti-virus') +Http::get('/v1/health/anti-virus') ->desc('Get antivirus') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -837,7 +875,7 @@ App::get('/v1/health/anti-virus') $response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS); }); -App::get('/v1/health/queue/failed/:name') +Http::get('/v1/health/queue/failed/:name') ->desc('Get number of failed queue jobs') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -878,7 +916,7 @@ App::get('/v1/health/queue/failed/:name') $response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/stats') // Currently only used internally +Http::get('/v1/health/stats') // Currently only used internally ->desc('Get system stats') ->groups(['api', 'health']) ->label('scope', 'root') diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 2917bc8416..fcaf0c03cb 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -3,12 +3,12 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Document; +use Utopia\Http\Http; use Utopia\Locale\Locale; -App::get('/v1/locale') +Http::get('/v1/locale') ->desc('Get user locale') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -68,8 +68,8 @@ App::get('/v1/locale') $response->dynamic(new Document($output), Response::MODEL_LOCALE); }); -App::get('/v1/locale/codes') - ->desc('List locale codes') +Http::get('/v1/locale/codes') + ->desc('List Locale Codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -90,7 +90,7 @@ App::get('/v1/locale/codes') ]), Response::MODEL_LOCALE_CODE_LIST); }); -App::get('/v1/locale/countries') +Http::get('/v1/locale/countries') ->desc('List countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -123,7 +123,7 @@ App::get('/v1/locale/countries') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -App::get('/v1/locale/countries/eu') +Http::get('/v1/locale/countries/eu') ->desc('List EU countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -158,7 +158,7 @@ App::get('/v1/locale/countries/eu') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -App::get('/v1/locale/countries/phones') +Http::get('/v1/locale/countries/phones') ->desc('List countries phone codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -192,7 +192,7 @@ App::get('/v1/locale/countries/phones') $response->dynamic(new Document(['phones' => $output, 'total' => \count($output)]), Response::MODEL_PHONE_LIST); }); -App::get('/v1/locale/continents') +Http::get('/v1/locale/continents') ->desc('List continents') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -224,7 +224,7 @@ App::get('/v1/locale/continents') $response->dynamic(new Document(['continents' => $output, 'total' => \count($output)]), Response::MODEL_CONTINENT_LIST); }); -App::get('/v1/locale/currencies') +Http::get('/v1/locale/currencies') ->desc('List currencies') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -247,7 +247,7 @@ App::get('/v1/locale/currencies') }); -App::get('/v1/locale/languages') +Http::get('/v1/locale/languages') ->desc('List languages') ->groups(['api', 'locale']) ->label('scope', 'locale.read') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 7da0348a8f..8beb38c7ca 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -20,7 +20,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Targets; use Appwrite\Utopia\Database\Validator\Queries\Topics; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -30,25 +29,27 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\JSON; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\Integer; -use Utopia\Validator\JSON; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use function Swoole\Coroutine\batch; -App::post('/v1/messaging/providers/mailgun') +Http::post('/v1/messaging/providers/mailgun') ->desc('Create Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -135,7 +136,7 @@ App::post('/v1/messaging/providers/mailgun') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/sendgrid') +Http::post('/v1/messaging/providers/sendgrid') ->desc('Create Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -210,7 +211,7 @@ App::post('/v1/messaging/providers/sendgrid') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/smtp') +Http::post('/v1/messaging/providers/smtp') ->desc('Create SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -298,7 +299,7 @@ App::post('/v1/messaging/providers/smtp') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/msg91') +Http::post('/v1/messaging/providers/msg91') ->desc('Create Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -374,7 +375,7 @@ App::post('/v1/messaging/providers/msg91') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/telesign') +Http::post('/v1/messaging/providers/telesign') ->desc('Create Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -451,7 +452,7 @@ App::post('/v1/messaging/providers/telesign') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/textmagic') +Http::post('/v1/messaging/providers/textmagic') ->desc('Create Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -528,7 +529,7 @@ App::post('/v1/messaging/providers/textmagic') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/twilio') +Http::post('/v1/messaging/providers/twilio') ->desc('Create Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -605,7 +606,7 @@ App::post('/v1/messaging/providers/twilio') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/vonage') +Http::post('/v1/messaging/providers/vonage') ->desc('Create Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -682,7 +683,7 @@ App::post('/v1/messaging/providers/vonage') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/fcm') +Http::post('/v1/messaging/providers/fcm') ->desc('Create FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -745,7 +746,7 @@ App::post('/v1/messaging/providers/fcm') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::post('/v1/messaging/providers/apns') +Http::post('/v1/messaging/providers/apns') ->desc('Create APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -831,7 +832,7 @@ App::post('/v1/messaging/providers/apns') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::get('/v1/messaging/providers') +Http::get('/v1/messaging/providers') ->desc('List providers') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -846,7 +847,8 @@ App::get('/v1/messaging/providers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -867,7 +869,7 @@ App::get('/v1/messaging/providers') if ($cursor) { $providerId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); @@ -882,7 +884,7 @@ App::get('/v1/messaging/providers') ]), Response::MODEL_PROVIDER_LIST); }); -App::get('/v1/messaging/providers/:providerId/logs') +Http::get('/v1/messaging/providers/:providerId/logs') ->desc('List provider logs') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -970,7 +972,7 @@ App::get('/v1/messaging/providers/:providerId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/providers/:providerId') +Http::get('/v1/messaging/providers/:providerId') ->desc('Get provider') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -994,7 +996,7 @@ App::get('/v1/messaging/providers/:providerId') $response->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/mailgun/:providerId') +Http::patch('/v1/messaging/providers/mailgun/:providerId') ->desc('Update Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1100,7 +1102,7 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/sendgrid/:providerId') +Http::patch('/v1/messaging/providers/sendgrid/:providerId') ->desc('Update Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1191,7 +1193,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/smtp/:providerId') +Http::patch('/v1/messaging/providers/smtp/:providerId') ->desc('Update SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1313,7 +1315,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/msg91/:providerId') +Http::patch('/v1/messaging/providers/msg91/:providerId') ->desc('Update Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1393,7 +1395,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/telesign/:providerId') +Http::patch('/v1/messaging/providers/telesign/:providerId') ->desc('Update Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1475,7 +1477,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/textmagic/:providerId') +Http::patch('/v1/messaging/providers/textmagic/:providerId') ->desc('Update Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1557,7 +1559,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/twilio/:providerId') +Http::patch('/v1/messaging/providers/twilio/:providerId') ->desc('Update Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1639,7 +1641,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/vonage/:providerId') +Http::patch('/v1/messaging/providers/vonage/:providerId') ->desc('Update Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1721,7 +1723,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/fcm/:providerId') +Http::patch('/v1/messaging/providers/fcm/:providerId') ->desc('Update FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1790,7 +1792,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId') }); -App::patch('/v1/messaging/providers/apns/:providerId') +Http::patch('/v1/messaging/providers/apns/:providerId') ->desc('Update APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1885,7 +1887,7 @@ App::patch('/v1/messaging/providers/apns/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::delete('/v1/messaging/providers/:providerId') +Http::delete('/v1/messaging/providers/:providerId') ->desc('Delete provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.delete') @@ -1920,7 +1922,7 @@ App::delete('/v1/messaging/providers/:providerId') ->noContent(); }); -App::post('/v1/messaging/topics') +Http::post('/v1/messaging/topics') ->desc('Create topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.create') @@ -1963,7 +1965,7 @@ App::post('/v1/messaging/topics') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::get('/v1/messaging/topics') +Http::get('/v1/messaging/topics') ->desc('List topics') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -1978,7 +1980,8 @@ App::get('/v1/messaging/topics') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -1999,7 +2002,7 @@ App::get('/v1/messaging/topics') if ($cursor) { $topicId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found."); @@ -2014,7 +2017,7 @@ App::get('/v1/messaging/topics') ]), Response::MODEL_TOPIC_LIST); }); -App::get('/v1/messaging/topics/:topicId/logs') +Http::get('/v1/messaging/topics/:topicId/logs') ->desc('List topic logs') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2031,7 +2034,8 @@ App::get('/v1/messaging/topics/:topicId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $topic = $dbForProject->getDocument('topics', $topicId); if ($topic->isEmpty()) { @@ -2103,7 +2107,7 @@ App::get('/v1/messaging/topics/:topicId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/topics/:topicId') +Http::get('/v1/messaging/topics/:topicId') ->desc('Get topic') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2128,7 +2132,7 @@ App::get('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::patch('/v1/messaging/topics/:topicId') +Http::patch('/v1/messaging/topics/:topicId') ->desc('Update topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.update') @@ -2172,7 +2176,7 @@ App::patch('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -App::delete('/v1/messaging/topics/:topicId') +Http::delete('/v1/messaging/topics/:topicId') ->desc('Delete topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.delete') @@ -2212,7 +2216,7 @@ App::delete('/v1/messaging/topics/:topicId') ->noContent(); }); -App::post('/v1/messaging/topics/:topicId/subscribers') +Http::post('/v1/messaging/topics/:topicId/subscribers') ->desc('Create subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.create') @@ -2232,28 +2236,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { $subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId; - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); } - $validator = new Authorization('subscribe'); - - if (!$validator->isValid($topic->getAttribute('subscribe'))) { - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); } - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber = new Document([ '$id' => $subscriberId, @@ -2286,7 +2289,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2308,7 +2311,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -App::get('/v1/messaging/topics/:topicId/subscribers') +Http::get('/v1/messaging/topics/:topicId/subscribers') ->desc('List subscribers') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2324,7 +2327,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2335,7 +2339,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $queries[] = Query::search('search', $search); } - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2353,7 +2357,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') if ($cursor) { $subscriberId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found."); @@ -2364,10 +2368,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $subscribers = $dbForProject->find('subscribers', $queries); - $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { - return function () use ($subscriber, $dbForProject) { - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) { + return function () use ($subscriber, $dbForProject, $authorization) { + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); return $subscriber ->setAttribute('target', $target) @@ -2382,7 +2386,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ]), Response::MODEL_SUBSCRIBER_LIST); }); -App::get('/v1/messaging/subscribers/:subscriberId/logs') +Http::get('/v1/messaging/subscribers/:subscriberId/logs') ->desc('List subscriber logs') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2399,7 +2403,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); if ($subscriber->isEmpty()) { @@ -2471,7 +2476,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Get subscriber') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2486,8 +2491,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->inject('authorization') + ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response, Authorization $authorization) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2499,8 +2505,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } - $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber ->setAttribute('target', $target) @@ -2510,7 +2516,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Delete subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.delete') @@ -2529,8 +2535,9 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { - $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->inject('authorization') + ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { + $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2553,7 +2560,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute( + $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2569,7 +2576,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->noContent(); }); -App::post('/v1/messaging/messages/email') +Http::post('/v1/messaging/messages/email') ->desc('Create email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2721,7 +2728,7 @@ App::post('/v1/messaging/messages/email') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::post('/v1/messaging/messages/sms') +Http::post('/v1/messaging/messages/sms') ->desc('Create SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2837,7 +2844,7 @@ App::post('/v1/messaging/messages/sms') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::post('/v1/messaging/messages/push') +Http::post('/v1/messaging/messages/push') ->desc('Create push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -3013,7 +3020,7 @@ App::post('/v1/messaging/messages/push') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::get('/v1/messaging/messages') +Http::get('/v1/messaging/messages') ->desc('List messages') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3028,7 +3035,8 @@ App::get('/v1/messaging/messages') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + ->inject('authorization') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -3049,7 +3057,7 @@ App::get('/v1/messaging/messages') if ($cursor) { $messageId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); @@ -3064,7 +3072,7 @@ App::get('/v1/messaging/messages') ]), Response::MODEL_MESSAGE_LIST); }); -App::get('/v1/messaging/messages/:messageId/logs') +Http::get('/v1/messaging/messages/:messageId/logs') ->desc('List message logs') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3081,7 +3089,8 @@ App::get('/v1/messaging/messages/:messageId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -3153,7 +3162,7 @@ App::get('/v1/messaging/messages/:messageId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/messages/:messageId/targets') +Http::get('/v1/messaging/messages/:messageId/targets') ->desc('List message targets') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3218,7 +3227,7 @@ App::get('/v1/messaging/messages/:messageId/targets') ]), Response::MODEL_TARGET_LIST); }); -App::get('/v1/messaging/messages/:messageId') +Http::get('/v1/messaging/messages/:messageId') ->desc('Get message') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3242,7 +3251,7 @@ App::get('/v1/messaging/messages/:messageId') $response->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/email/:messageId') +Http::patch('/v1/messaging/messages/email/:messageId') ->desc('Update email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3442,7 +3451,7 @@ App::patch('/v1/messaging/messages/email/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/sms/:messageId') +Http::patch('/v1/messaging/messages/sms/:messageId') ->desc('Update SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3597,7 +3606,7 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::patch('/v1/messaging/messages/push/:messageId') +Http::patch('/v1/messaging/messages/push/:messageId') ->desc('Update push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3835,7 +3844,7 @@ App::patch('/v1/messaging/messages/push/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -App::delete('/v1/messaging/messages/:messageId') +Http::delete('/v1/messaging/messages/:messageId') ->desc('Delete message') ->groups(['api', 'messaging']) ->label('audits.event', 'message.delete') diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 374a5575a7..552a4293f1 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -9,7 +9,6 @@ use Appwrite\Role; use Appwrite\Utopia\Database\Validator\Queries\Migrations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -17,21 +16,22 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\Firebase; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Host; -use Utopia\Validator\Integer; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; include_once __DIR__ . '/../shared/api.php'; -App::post('/v1/migrations/appwrite') +Http::post('/v1/migrations/appwrite') ->groups(['api', 'migrations']) ->desc('Migrate Appwrite data') ->label('scope', 'migrations.write') @@ -85,7 +85,7 @@ App::post('/v1/migrations/appwrite') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/firebase/oauth') +Http::post('/v1/migrations/firebase/oauth') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (OAuth)') ->label('scope', 'migrations.write') @@ -187,7 +187,7 @@ App::post('/v1/migrations/firebase/oauth') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/firebase') +Http::post('/v1/migrations/firebase') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (Service Account)') ->label('scope', 'migrations.write') @@ -247,7 +247,7 @@ App::post('/v1/migrations/firebase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/supabase') +Http::post('/v1/migrations/supabase') ->groups(['api', 'migrations']) ->desc('Migrate Supabase data') ->label('scope', 'migrations.write') @@ -307,7 +307,7 @@ App::post('/v1/migrations/supabase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/nhost') +Http::post('/v1/migrations/nhost') ->groups(['api', 'migrations']) ->desc('Migrate NHost data') ->label('scope', 'migrations.write') @@ -369,7 +369,7 @@ App::post('/v1/migrations/nhost') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::get('/v1/migrations') +Http::get('/v1/migrations') ->groups(['api', 'migrations']) ->desc('List migrations') ->label('scope', 'migrations.read') @@ -422,7 +422,7 @@ App::get('/v1/migrations') ]), Response::MODEL_MIGRATION_LIST); }); -App::get('/v1/migrations/:migrationId') +Http::get('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Get migration') ->label('scope', 'migrations.read') @@ -446,7 +446,7 @@ App::get('/v1/migrations/:migrationId') $response->dynamic($migration, Response::MODEL_MIGRATION); }); -App::get('/v1/migrations/appwrite/report') +Http::get('/v1/migrations/appwrite/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Appwrite data') ->label('scope', 'migrations.write') @@ -488,7 +488,7 @@ App::get('/v1/migrations/appwrite/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/report') +Http::get('/v1/migrations/firebase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data') ->label('scope', 'migrations.write') @@ -535,7 +535,7 @@ App::get('/v1/migrations/firebase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/report/oauth') +Http::get('/v1/migrations/firebase/report/oauth') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data using OAuth') ->label('scope', 'migrations.write') @@ -626,7 +626,7 @@ App::get('/v1/migrations/firebase/report/oauth') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/firebase/connect') +Http::get('/v1/migrations/firebase/connect') ->desc('Authorize with Firebase') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') @@ -668,7 +668,7 @@ App::get('/v1/migrations/firebase/connect') ->redirect($url); }); -App::get('/v1/migrations/firebase/redirect') +Http::get('/v1/migrations/firebase/redirect') ->desc('Capture and receive data on Firebase authorization') ->groups(['api', 'migrations']) ->label('scope', 'public') @@ -780,8 +780,8 @@ App::get('/v1/migrations/firebase/redirect') ->redirect($redirect); }); -App::get('/v1/migrations/firebase/projects') - ->desc('List Firebase projects') +Http::get('/v1/migrations/firebase/projects') + ->desc('List Firebase Projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -869,8 +869,8 @@ App::get('/v1/migrations/firebase/projects') ]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST); }); -App::get('/v1/migrations/firebase/deauthorize') - ->desc('Revoke Appwrite\'s authorization to access Firebase projects') +Http::get('/v1/migrations/firebase/deauthorize') + ->desc('Revoke Appwrite\'s authorization to access Firebase Projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -897,7 +897,7 @@ App::get('/v1/migrations/firebase/deauthorize') $response->noContent(); }); -App::get('/v1/migrations/supabase/report') +Http::get('/v1/migrations/supabase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Supabase Data') ->label('scope', 'migrations.write') @@ -940,7 +940,7 @@ App::get('/v1/migrations/supabase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::get('/v1/migrations/nhost/report') +Http::get('/v1/migrations/nhost/report') ->groups(['api', 'migrations']) ->desc('Generate a report on NHost Data') ->label('scope', 'migrations.write') @@ -983,7 +983,7 @@ App::get('/v1/migrations/nhost/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -App::patch('/v1/migrations/:migrationId') +Http::patch('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Retry migration') ->label('scope', 'migrations.write') @@ -1028,7 +1028,7 @@ App::patch('/v1/migrations/:migrationId') $response->noContent(); }); -App::delete('/v1/migrations/:migrationId') +Http::delete('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Delete migration') ->label('scope', 'migrations.write') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 6053326308..8a6f4d942e 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -2,7 +2,6 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; @@ -13,10 +12,11 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DateTimeValidator; use Utopia\Database\Validator\UID; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; -App::get('/v1/project/usage') +Http::get('/v1/project/usage') ->desc('Get project usage stats') ->groups(['api', 'usage']) ->label('scope', 'projects.read') @@ -31,7 +31,8 @@ App::get('/v1/project/usage') ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject, Authorization $authorization) { $stats = $total = $usage = []; $format = 'Y-m-d 00:00:00'; $firstDay = (new DateTime($startDate))->format($format); @@ -78,7 +79,7 @@ App::get('/v1/project/usage') '1d' => 'Y-m-d\T00:00:00.000P', }; - Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { + $authorization->skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { foreach ($metrics['total'] as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -296,8 +297,6 @@ App::get('/v1/project/usage') 'buildsStorageTotal' => $total[METRIC_BUILDS_STORAGE], 'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE], 'executionsBreakdown' => $executionsBreakdown, - 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, - 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, @@ -308,8 +307,8 @@ App::get('/v1/project/usage') // Variables -App::post('/v1/project/variables') - ->desc('Create variable') +Http::post('/v1/project/variables') + ->desc('Create Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('audits.event', 'variable.create') @@ -363,8 +362,8 @@ App::post('/v1/project/variables') ->dynamic($variable, Response::MODEL_VARIABLE); }); -App::get('/v1/project/variables') - ->desc('List variables') +Http::get('/v1/project/variables') + ->desc('List Variables') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -388,8 +387,8 @@ App::get('/v1/project/variables') ]), Response::MODEL_VARIABLE_LIST); }); -App::get('/v1/project/variables/:variableId') - ->desc('Get variable') +Http::get('/v1/project/variables/:variableId') + ->desc('Get Variable') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -412,8 +411,8 @@ App::get('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::put('/v1/project/variables/:variableId') - ->desc('Update variable') +Http::put('/v1/project/variables/:variableId') + ->desc('Update Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -458,8 +457,8 @@ App::put('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -App::delete('/v1/project/variables/:variableId') - ->desc('Delete variable') +Http::delete('/v1/project/variables/:variableId') + ->desc('Delete Variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 3a8c232195..da5b4b882c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -13,14 +13,16 @@ use Appwrite\Network\Validator\Origin; use Appwrite\Template\Template; use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -30,24 +32,25 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; use Utopia\DSN\DSN; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Hostname; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Multiple; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\URL; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; -use Utopia\Pools\Group; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\Hostname; -use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\URL; -use Utopia\Validator\WhiteList; -App::init() +Http::init() ->groups(['projects']) ->inject('project') ->action(function (Document $project) { @@ -56,7 +59,7 @@ App::init() } }); -App::post('/v1/projects') +Http::post('/v1/projects') ->desc('Create project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.create') @@ -86,8 +89,9 @@ App::post('/v1/projects') ->inject('cache') ->inject('pools') ->inject('hooks') - ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) { - + ->inject('authorization') + ->inject('connections') + ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, array $pools, Hooks $hooks, Authorization $authorization, Connections $connections) { $team = $dbForConsole->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -189,8 +193,21 @@ App::post('/v1/projects') $dsn = new DSN('mysql://' . $dsn); } - $adapter = $pools->get($dsn->getHost())->pop()->getResource(); + $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($connectionDsn->getPath()); + $dbForProject = new Database($adapter, $cache); + $dbForProject->setAuthorization($authorization); if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { $dbForProject @@ -208,10 +225,8 @@ App::post('/v1/projects') $audit = new Audit($dbForProject); $audit->setup(); - $abuse = new TimeLimit('', 0, 1, $dbForProject); $abuse->setup(); - /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; @@ -234,17 +249,17 @@ App::post('/v1/projects') // Collection already exists } } - + $connections->reclaim(); // Hook allowing instant project mirroring during migration // Outside of migration, hook is not registered and has no effect - $hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]); + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); }); -App::get('/v1/projects') +Http::get('/v1/projects') ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -259,7 +274,6 @@ App::get('/v1/projects') ->inject('response') ->inject('dbForConsole') ->action(function (array $queries, string $search, Response $response, Database $dbForConsole) { - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -297,7 +311,7 @@ App::get('/v1/projects') ]), Response::MODEL_PROJECT_LIST); }); -App::get('/v1/projects/:projectId') +Http::get('/v1/projects/:projectId') ->desc('Get project') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -311,7 +325,6 @@ App::get('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -321,7 +334,7 @@ App::get('/v1/projects/:projectId') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId') +Http::patch('/v1/projects/:projectId') ->desc('Update project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -345,31 +358,34 @@ App::patch('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('name', $name) - ->setAttribute('description', $description) - ->setAttribute('logo', $logo) - ->setAttribute('url', $url) - ->setAttribute('legalName', $legalName) - ->setAttribute('legalCountry', $legalCountry) - ->setAttribute('legalState', $legalState) - ->setAttribute('legalCity', $legalCity) - ->setAttribute('legalAddress', $legalAddress) - ->setAttribute('legalTaxId', $legalTaxId) - ->setAttribute('search', implode(' ', [$projectId, $name]))); + $project = $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('name', $name) + ->setAttribute('description', $description) + ->setAttribute('logo', $logo) + ->setAttribute('url', $url) + ->setAttribute('legalName', $legalName) + ->setAttribute('legalCountry', $legalCountry) + ->setAttribute('legalState', $legalState) + ->setAttribute('legalCity', $legalCity) + ->setAttribute('legalAddress', $legalAddress) + ->setAttribute('legalTaxId', $legalTaxId) + ->setAttribute('search', implode(' ', [$projectId, $name])) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/team') - ->desc('Update project team') +Http::patch('/v1/projects/:projectId/team') + ->desc('Update Project Team') ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -383,7 +399,6 @@ App::patch('/v1/projects/:projectId/team') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); $team = $dbForConsole->getDocument('teams', $teamId); @@ -436,7 +451,7 @@ App::patch('/v1/projects/:projectId/team') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/service') +Http::patch('/v1/projects/:projectId/service') ->desc('Update service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -452,7 +467,6 @@ App::patch('/v1/projects/:projectId/service') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -467,7 +481,7 @@ App::patch('/v1/projects/:projectId/service') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/service/all') +Http::patch('/v1/projects/:projectId/service/all') ->desc('Update all service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -482,7 +496,6 @@ App::patch('/v1/projects/:projectId/service/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -501,7 +514,7 @@ App::patch('/v1/projects/:projectId/service/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/api') +Http::patch('/v1/projects/:projectId/api') ->desc('Update API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -517,7 +530,6 @@ App::patch('/v1/projects/:projectId/api') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -532,7 +544,7 @@ App::patch('/v1/projects/:projectId/api') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/api/all') +Http::patch('/v1/projects/:projectId/api/all') ->desc('Update all API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -547,7 +559,6 @@ App::patch('/v1/projects/:projectId/api/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -566,7 +577,7 @@ App::patch('/v1/projects/:projectId/api/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/oauth2') +Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -584,7 +595,6 @@ App::patch('/v1/projects/:projectId/oauth2') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -610,7 +620,7 @@ App::patch('/v1/projects/:projectId/oauth2') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/session-alerts') +Http::patch('/v1/projects/:projectId/auth/session-alerts') ->desc('Update project sessions emails') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -641,7 +651,7 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/limit') +Http::patch('/v1/projects/:projectId/auth/limit') ->desc('Update project users limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -656,7 +666,6 @@ App::patch('/v1/projects/:projectId/auth/limit') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -666,13 +675,17 @@ App::patch('/v1/projects/:projectId/auth/limit') $auths = $project->getAttribute('auths', []); $auths['limit'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/duration') +Http::patch('/v1/projects/:projectId/auth/duration') ->desc('Update project authentication duration') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -687,7 +700,6 @@ App::patch('/v1/projects/:projectId/auth/duration') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -697,13 +709,17 @@ App::patch('/v1/projects/:projectId/auth/duration') $auths = $project->getAttribute('auths', []); $auths['duration'] = $duration; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/:method') +Http::patch('/v1/projects/:projectId/auth/:method') ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -719,10 +735,9 @@ App::patch('/v1/projects/:projectId/auth/:method') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); - $auth = Config::getParam('auth')[$method] ?? []; - $authKey = $auth['key'] ?? ''; + $authConfig = Config::getParam('auth')[$method] ?? []; + $authKey = $authConfig['key'] ?? ''; $status = ($status === '1' || $status === 'true' || $status === 1 || $status === true); if ($project->isEmpty()) { @@ -737,7 +752,7 @@ App::patch('/v1/projects/:projectId/auth/:method') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/password-history') +Http::patch('/v1/projects/:projectId/auth/password-history') ->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -752,7 +767,6 @@ App::patch('/v1/projects/:projectId/auth/password-history') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -762,13 +776,17 @@ App::patch('/v1/projects/:projectId/auth/password-history') $auths = $project->getAttribute('auths', []); $auths['passwordHistory'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/password-dictionary') +Http::patch('/v1/projects/:projectId/auth/password-dictionary') ->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -783,7 +801,6 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -793,13 +810,17 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') $auths = $project->getAttribute('auths', []); $auths['passwordDictionary'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/personal-data') +Http::patch('/v1/projects/:projectId/auth/personal-data') ->desc('Enable or disable checking user passwords for similarity with their personal data.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -814,7 +835,6 @@ App::patch('/v1/projects/:projectId/auth/personal-data') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -824,13 +844,17 @@ App::patch('/v1/projects/:projectId/auth/personal-data') $auths = $project->getAttribute('auths', []); $auths['personalDataCheck'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/max-sessions') +Http::patch('/v1/projects/:projectId/auth/max-sessions') ->desc('Update project user sessions limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -845,7 +869,6 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -855,13 +878,17 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') $auths = $project->getAttribute('auths', []); $auths['maxSessions'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project - ->setAttribute('auths', $auths)); + $dbForConsole->updateDocument( + 'projects', + $project->getId(), + $project + ->setAttribute('auths', $auths) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); -App::patch('/v1/projects/:projectId/auth/mock-numbers') +Http::patch('/v1/projects/:projectId/auth/mock-numbers') ->desc('Update the mock numbers for the project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -900,7 +927,7 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::delete('/v1/projects/:projectId') +Http::delete('/v1/projects/:projectId') ->desc('Delete project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.delete') @@ -935,7 +962,7 @@ App::delete('/v1/projects/:projectId') // Webhooks -App::post('/v1/projects/:projectId/webhooks') +Http::post('/v1/projects/:projectId/webhooks') ->desc('Create webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -956,14 +983,13 @@ App::post('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); + $security = (bool)filter_var($security, FILTER_VALIDATE_BOOLEAN); $webhook = new Document([ '$id' => ID::unique(), @@ -993,7 +1019,7 @@ App::post('/v1/projects/:projectId/webhooks') ->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::get('/v1/projects/:projectId/webhooks') +Http::get('/v1/projects/:projectId/webhooks') ->desc('List webhooks') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1007,7 +1033,6 @@ App::get('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1025,7 +1050,7 @@ App::get('/v1/projects/:projectId/webhooks') ]), Response::MODEL_WEBHOOK_LIST); }); -App::get('/v1/projects/:projectId/webhooks/:webhookId') +Http::get('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1040,7 +1065,6 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1059,7 +1083,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::put('/v1/projects/:projectId/webhooks/:webhookId') +Http::put('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1081,7 +1105,6 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1118,7 +1141,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') +Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->desc('Update webhook signature key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1133,7 +1156,6 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1157,7 +1179,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -App::delete('/v1/projects/:projectId/webhooks/:webhookId') +Http::delete('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1171,7 +1193,6 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1196,7 +1217,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') // Keys -App::post('/v1/projects/:projectId/keys') +Http::post('/v1/projects/:projectId/keys') ->desc('Create key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1213,7 +1234,6 @@ App::post('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1246,7 +1266,7 @@ App::post('/v1/projects/:projectId/keys') ->dynamic($key, Response::MODEL_KEY); }); -App::get('/v1/projects/:projectId/keys') +Http::get('/v1/projects/:projectId/keys') ->desc('List keys') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1260,7 +1280,6 @@ App::get('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1278,7 +1297,7 @@ App::get('/v1/projects/:projectId/keys') ]), Response::MODEL_KEY_LIST); }); -App::get('/v1/projects/:projectId/keys/:keyId') +Http::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get key') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1293,7 +1312,6 @@ App::get('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1312,7 +1330,7 @@ App::get('/v1/projects/:projectId/keys/:keyId') $response->dynamic($key, Response::MODEL_KEY); }); -App::put('/v1/projects/:projectId/keys/:keyId') +Http::put('/v1/projects/:projectId/keys/:keyId') ->desc('Update key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1330,7 +1348,6 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1358,7 +1375,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') $response->dynamic($key, Response::MODEL_KEY); }); -App::delete('/v1/projects/:projectId/keys/:keyId') +Http::delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1372,7 +1389,6 @@ App::delete('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1397,7 +1413,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId') // JWT Keys -App::post('/v1/projects/:projectId/jwts') +Http::post('/v1/projects/:projectId/jwts') ->groups(['api', 'projects']) ->desc('Create JWT') ->label('scope', 'projects.write') @@ -1432,7 +1448,7 @@ App::post('/v1/projects/:projectId/jwts') // Platforms -App::post('/v1/projects/:projectId/platforms') +Http::post('/v1/projects/:projectId/platforms') ->desc('Create platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.create') @@ -1483,7 +1499,7 @@ App::post('/v1/projects/:projectId/platforms') ->dynamic($platform, Response::MODEL_PLATFORM); }); -App::get('/v1/projects/:projectId/platforms') +Http::get('/v1/projects/:projectId/platforms') ->desc('List platforms') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1497,7 +1513,6 @@ App::get('/v1/projects/:projectId/platforms') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1515,7 +1530,7 @@ App::get('/v1/projects/:projectId/platforms') ]), Response::MODEL_PLATFORM_LIST); }); -App::get('/v1/projects/:projectId/platforms/:platformId') +Http::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get platform') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1530,7 +1545,6 @@ App::get('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1549,7 +1563,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId') $response->dynamic($platform, Response::MODEL_PLATFORM); }); -App::put('/v1/projects/:projectId/platforms/:platformId') +Http::put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update platform') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1596,7 +1610,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId') $response->dynamic($platform, Response::MODEL_PLATFORM); }); -App::delete('/v1/projects/:projectId/platforms/:platformId') +Http::delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.delete') @@ -1611,7 +1625,6 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1636,7 +1649,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') // CUSTOM SMTP and Templates -App::patch('/v1/projects/:projectId/smtp') +Http::patch('/v1/projects/:projectId/smtp') ->desc('Update SMTP') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1659,7 +1672,6 @@ App::patch('/v1/projects/:projectId/smtp') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1726,7 +1738,7 @@ App::patch('/v1/projects/:projectId/smtp') $response->dynamic($project, Response::MODEL_PROJECT); }); -App::post('/v1/projects/:projectId/smtp/tests') +Http::post('/v1/projects/:projectId/smtp/tests') ->desc('Create SMTP test') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1785,7 +1797,7 @@ App::post('/v1/projects/:projectId/smtp/tests') return $response->noContent(); }); -App::get('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Get custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1801,7 +1813,6 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1811,7 +1822,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { $template = [ @@ -1826,7 +1837,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') }); -App::get('/v1/projects/:projectId/templates/email/:type/:locale') +Http::get('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Get custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1842,7 +1853,6 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1850,7 +1860,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; $localeObj = new Locale($locale); if (is_null($template)) { @@ -1858,7 +1868,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') $message ->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello")) ->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer")) - ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false) + ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escape: false) ->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks")) ->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature")) ->setParam('{{direction}}', $localeObj->getText('settings.direction')); @@ -1878,7 +1888,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') $response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE); }); -App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Update custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1895,7 +1905,6 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1918,7 +1927,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -App::patch('/v1/projects/:projectId/templates/email/:type/:locale') +Http::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Update custom email templates') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1939,7 +1948,6 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1968,7 +1976,7 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ]), Response::MODEL_EMAIL_TEMPLATE); }); -App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') +Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Reset custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1984,7 +1992,6 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1994,7 +2001,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); @@ -2011,7 +2018,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -App::delete('/v1/projects/:projectId/templates/email/:type/:locale') +Http::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Reset custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -2027,7 +2034,6 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -2035,7 +2041,7 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 84484a7209..3ca019c9ce 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -7,7 +7,6 @@ use Appwrite\Extend\Exception; use Appwrite\Network\Validator\CNAME; use Appwrite\Utopia\Database\Validator\Queries\Rules; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; @@ -15,13 +14,14 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; +use Utopia\Http\Http; +use Utopia\Http\Validator\Domain as ValidatorDomain; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Logger\Log; use Utopia\System\System; -use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::post('/v1/proxy/rules') +Http::post('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('Create rule') ->label('scope', 'rules.write') @@ -147,7 +147,7 @@ App::post('/v1/proxy/rules') ->dynamic($rule, Response::MODEL_PROXY_RULE); }); -App::get('/v1/proxy/rules') +Http::get('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('List rules') ->label('scope', 'rules.read') @@ -210,7 +210,7 @@ App::get('/v1/proxy/rules') ]), Response::MODEL_PROXY_RULE_LIST); }); -App::get('/v1/proxy/rules/:ruleId') +Http::get('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Get rule') ->label('scope', 'rules.read') @@ -239,7 +239,7 @@ App::get('/v1/proxy/rules/:ruleId') $response->dynamic($rule, Response::MODEL_PROXY_RULE); }); -App::delete('/v1/proxy/rules/:ruleId') +Http::delete('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Delete rule') ->label('scope', 'rules.write') @@ -276,8 +276,8 @@ App::delete('/v1/proxy/rules/:ruleId') $response->noContent(); }); -App::patch('/v1/proxy/rules/:ruleId/verification') - ->desc('Update rule verification status') +Http::patch('/v1/proxy/rules/:ruleId/verification') + ->desc('Update Rule Verification Status') ->groups(['api', 'proxy']) ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].update') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index ca248c2628..e0408acb37 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -11,8 +11,8 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Buckets; use Appwrite\Utopia\Database\Validator\Queries\Files; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -23,8 +23,16 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\HexColor; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; use Utopia\Storage\Compression\Algorithms\GZIP; use Utopia\Storage\Compression\Algorithms\Zstd; @@ -35,16 +43,9 @@ use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; -use Utopia\Swoole\Request; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Boolean; -use Utopia\Validator\HexColor; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; -App::post('/v1/storage/buckets') +Http::post('/v1/storage/buckets') ->desc('Create bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -142,7 +143,7 @@ App::post('/v1/storage/buckets') ->dynamic($bucket, Response::MODEL_BUCKET); }); -App::get('/v1/storage/buckets') +Http::get('/v1/storage/buckets') ->desc('List buckets') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -196,7 +197,7 @@ App::get('/v1/storage/buckets') ]), Response::MODEL_BUCKET_LIST); }); -App::get('/v1/storage/buckets/:bucketId') +Http::get('/v1/storage/buckets/:bucketId') ->desc('Get bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -221,7 +222,7 @@ App::get('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -App::put('/v1/storage/buckets/:bucketId') +Http::put('/v1/storage/buckets/:bucketId') ->desc('Update bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -288,7 +289,7 @@ App::put('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -App::delete('/v1/storage/buckets/:bucketId') +Http::delete('/v1/storage/buckets/:bucketId') ->desc('Delete bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -329,7 +330,7 @@ App::delete('/v1/storage/buckets/:bucketId') $response->noContent(); }); -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('Create file') ->groups(['api', 'storage']) @@ -361,19 +362,19 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -397,7 +398,7 @@ App::post('/v1/storage/buckets/:bucketId/files') } // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -410,7 +411,7 @@ App::post('/v1/storage/buckets/:bucketId/files') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -630,11 +631,10 @@ App::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } else { if ($file->isEmpty()) { @@ -669,11 +669,10 @@ App::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - $validator = new Authorization(Database::PERMISSION_CREATE); - if (!$validator->isValid($bucket->getCreate())) { + if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } @@ -690,7 +689,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->dynamic($file, Response::MODEL_FILE); }); -App::get('/v1/storage/buckets/:bucketId/files') +Http::get('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('List files') ->groups(['api', 'storage']) @@ -708,19 +707,19 @@ App::get('/v1/storage/buckets/:bucketId/files') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -749,7 +748,7 @@ App::get('/v1/storage/buckets/:bucketId/files') if ($fileSecurity && !$valid) { $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($cursorDocument->isEmpty()) { @@ -765,8 +764,8 @@ App::get('/v1/storage/buckets/:bucketId/files') $files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries); $total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { - $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); - $total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); + $files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); + $total = $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); } $response->dynamic(new Document([ @@ -775,7 +774,7 @@ App::get('/v1/storage/buckets/:bucketId/files') ]), Response::MODEL_FILE_LIST); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Get file') ->groups(['api', 'storage']) @@ -792,19 +791,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -812,7 +811,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -822,7 +821,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->alias('/v1/storage/files/:fileId/preview', ['bucketId' => 'default']) ->desc('Get file preview') ->groups(['api', 'storage']) @@ -857,24 +856,24 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); } - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -882,7 +881,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -998,7 +997,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') unset($image); }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->alias('/v1/storage/files/:fileId/download', ['bucketId' => 'default']) ->desc('Get file for download') ->groups(['api', 'storage']) @@ -1017,20 +1016,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1038,7 +1037,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1138,7 +1137,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->alias('/v1/storage/files/:fileId/view', ['bucketId' => 'default']) ->desc('Get file for view') ->groups(['api', 'storage']) @@ -1157,19 +1156,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1177,7 +1176,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1290,7 +1289,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') } }); -App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->desc('Get file for push notification') ->groups(['api', 'storage']) ->label('scope', 'public') @@ -1306,8 +1305,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->inject('project') ->inject('mode') ->inject('deviceForFiles') - ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); @@ -1325,14 +1325,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') throw new Exception(Exception::USER_UNAUTHORIZED); } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1443,7 +1443,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') } }); -App::put('/v1/storage/buckets/:bucketId/files/:fileId') +Http::put('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Update file') ->groups(['api', 'storage']) @@ -1470,26 +1470,26 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('user') ->inject('mode') ->inject('queueForEvents') - ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents, Authorization $authorization) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_UPDATE); - $valid = $validator->isValid($bucket->getUpdate()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for update - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1503,7 +1503,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -1516,7 +1516,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!Authorization::isRole($role)) { + if (!$authorization->isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -1536,7 +1536,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file); } else { - $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } $queueForEvents @@ -1548,8 +1548,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -App::delete('/v1/storage/buckets/:bucketId/files/:fileId') - ->desc('Delete file') +Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') + ->desc('Delete File') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].delete') @@ -1572,32 +1572,32 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('mode') ->inject('deviceForFiles') ->inject('queueForDeletes') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->inject('authorization') + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes, Authorization $authorization) { + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_DELETE); - $valid = $validator->isValid($bucket->getDelete()); + $valid = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $bucket->getDelete())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for delete - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } // Make sure we don't delete the file before the document permission check occurs - if ($fileSecurity && !$valid && !$validator->isValid($file->getDelete())) { + if ($fileSecurity && !$valid && !$authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()))) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1621,7 +1621,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if (!$deleted) { @@ -1641,7 +1641,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') $response->noContent(); }); -App::get('/v1/storage/usage') +Http::get('/v1/storage/usage') ->desc('Get storage usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1654,7 +1654,8 @@ App::get('/v1/storage/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -1666,7 +1667,7 @@ App::get('/v1/storage/usage') ]; $total = []; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -1720,7 +1721,7 @@ App::get('/v1/storage/usage') ]), Response::MODEL_USAGE_STORAGE); }); -App::get('/v1/storage/:bucketId/usage') +Http::get('/v1/storage/:bucketId/usage') ->desc('Get bucket usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1734,7 +1735,8 @@ App::get('/v1/storage/:bucketId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -1750,8 +1752,7 @@ App::get('/v1/storage/:bucketId/usage') str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE), ]; - - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f98cdd721c..16529d3dbd 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,7 @@ desc('Create team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].create') @@ -65,15 +66,16 @@ App::post('/v1/teams') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; try { - $team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([ + $team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([ '$id' => $teamId, '$permissions' => [ Permission::read(Role::team($teamId)), @@ -132,7 +134,7 @@ App::post('/v1/teams') ->dynamic($team, Response::MODEL_TEAM); }); -App::get('/v1/teams') +Http::get('/v1/teams') ->desc('List teams') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -190,7 +192,7 @@ App::get('/v1/teams') ]), Response::MODEL_TEAM_LIST); }); -App::get('/v1/teams/:teamId') +Http::get('/v1/teams/:teamId') ->desc('Get team') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -217,7 +219,7 @@ App::get('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -App::get('/v1/teams/:teamId/prefs') +Http::get('/v1/teams/:teamId/prefs') ->desc('Get team preferences') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -245,7 +247,7 @@ App::get('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::put('/v1/teams/:teamId') +Http::put('/v1/teams/:teamId') ->desc('Update name') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update') @@ -288,7 +290,7 @@ App::put('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -App::put('/v1/teams/:teamId/prefs') +Http::put('/v1/teams/:teamId/prefs') ->desc('Update preferences') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update.prefs') @@ -324,7 +326,7 @@ App::put('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::delete('/v1/teams/:teamId') +Http::delete('/v1/teams/:teamId') ->desc('Delete team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].delete') @@ -373,7 +375,7 @@ App::delete('/v1/teams/:teamId') $response->noContent(); }); -App::post('/v1/teams/:teamId/memberships') +Http::post('/v1/teams/:teamId/memberships') ->desc('Create team membership') ->groups(['api', 'teams', 'auth']) ->label('event', 'teams.[teamId].memberships.[membershipId].create') @@ -405,9 +407,10 @@ App::post('/v1/teams/:teamId/memberships') ->inject('queueForMails') ->inject('queueForMessaging') ->inject('queueForEvents') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + ->inject('authorization') + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, Authorization $authorization) { + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -419,8 +422,8 @@ App::post('/v1/teams/:teamId/memberships') if (empty($userId) && empty($email) && empty($phone)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); if (!$isPrivilegedUser && !$isAppUser && empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED); @@ -480,7 +483,7 @@ App::post('/v1/teams/:teamId/memberships') try { $userId = ID::unique(); - $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ + $invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, '$permissions' => [ Permission::read(Role::any()), @@ -516,7 +519,7 @@ App::post('/v1/teams/:teamId/memberships') } } - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team'); @@ -548,12 +551,12 @@ App::post('/v1/teams/:teamId/memberships') if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership try { - $membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)); + $membership = $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { @@ -575,7 +578,7 @@ App::post('/v1/teams/:teamId/memberships') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{body}}', $body, escape: false) ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) @@ -693,7 +696,7 @@ App::post('/v1/teams/:teamId/memberships') ); }); -App::get('/v1/teams/:teamId/memberships') +Http::get('/v1/teams/:teamId/memberships') ->desc('List team memberships') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -796,7 +799,7 @@ App::get('/v1/teams/:teamId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -App::get('/v1/teams/:teamId/memberships/:membershipId') +Http::get('/v1/teams/:teamId/memberships/:membershipId') ->desc('Get team membership') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -852,7 +855,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') $response->dynamic($membership, Response::MODEL_MEMBERSHIP); }); -App::patch('/v1/teams/:teamId/memberships/:membershipId') +Http::patch('/v1/teams/:teamId/memberships/:membershipId') ->desc('Update membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update') @@ -874,7 +877,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -891,9 +895,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); - $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles'); @@ -924,7 +928,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ); }); -App::patch('/v1/teams/:teamId/memberships/:membershipId/status') +Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->desc('Update team membership status') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update.status') @@ -950,7 +954,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('project') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) { + ->inject('authorization') + ->inject('authentication') + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -959,7 +965,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') throw new Exception(Exception::MEMBERSHIP_NOT_FOUND); } - $team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId)); + $team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId)); if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); @@ -994,11 +1000,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->setAttribute('confirm', true) ; - Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); + $authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); // Log user in - Authorization::setRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::user($user->getId())->toString()); $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); @@ -1028,13 +1034,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $dbForProject->purgeCachedDocument('users', $user->getId()); - Authorization::setRole(Role::user($userId)->toString()); + $authorization->addRole(Role::user($userId)->toString()); $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); $dbForProject->purgeCachedDocument('users', $user->getId()); - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $queueForEvents ->setParam('userId', $user->getId()) @@ -1044,13 +1050,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) ; } $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic( @@ -1062,7 +1068,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ); }); -App::delete('/v1/teams/:teamId/memberships/:membershipId') +Http::delete('/v1/teams/:teamId/memberships/:membershipId') ->desc('Delete team membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].delete') @@ -1080,7 +1086,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents) { + ->inject('authorization') + ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1115,7 +1122,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') $dbForProject->purgeCachedDocument('users', $user->getId()); if ($membership->getAttribute('confirm')) { // Count only confirmed members - Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); + $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); } $queueForEvents @@ -1128,7 +1135,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') $response->noContent(); }); -App::get('/v1/teams/:teamId/logs') +Http::get('/v1/teams/:teamId/logs') ->desc('List team logs') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -1145,7 +1152,8 @@ App::get('/v1/teams/:teamId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $team = $dbForProject->getDocument('teams', $teamId); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 571df4fdb2..3d080fd98e 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -22,7 +22,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Users; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -39,15 +38,16 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Assoc; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Integer; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; -use Utopia\Validator\Boolean; -use Utopia\Validator\Integer; -use Utopia\Validator\Range; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; /** TODO: Remove function when we move to using utopia/platform */ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document @@ -180,7 +180,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e return $user; } -App::post('/v1/users') +Http::post('/v1/users') ->desc('Create user') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -211,7 +211,7 @@ App::post('/v1/users') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/bcrypt') +Http::post('/v1/users/bcrypt') ->desc('Create user with bcrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -242,7 +242,7 @@ App::post('/v1/users/bcrypt') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/md5') +Http::post('/v1/users/md5') ->desc('Create user with MD5 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -273,7 +273,7 @@ App::post('/v1/users/md5') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/argon2') +Http::post('/v1/users/argon2') ->desc('Create user with Argon2 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -304,7 +304,7 @@ App::post('/v1/users/argon2') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/sha') +Http::post('/v1/users/sha') ->desc('Create user with SHA password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -342,7 +342,7 @@ App::post('/v1/users/sha') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/phpass') +Http::post('/v1/users/phpass') ->desc('Create user with PHPass password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -373,7 +373,7 @@ App::post('/v1/users/phpass') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/scrypt') +Http::post('/v1/users/scrypt') ->desc('Create user with Scrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -417,7 +417,7 @@ App::post('/v1/users/scrypt') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/scrypt-modified') +Http::post('/v1/users/scrypt-modified') ->desc('Create user with Scrypt modified password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -451,7 +451,7 @@ App::post('/v1/users/scrypt-modified') ->dynamic($user, Response::MODEL_USER); }); -App::post('/v1/users/:userId/targets') +Http::post('/v1/users/:userId/targets') ->desc('Create user target') ->groups(['api', 'users']) ->label('audits.event', 'target.create') @@ -540,7 +540,7 @@ App::post('/v1/users/:userId/targets') ->dynamic($target, Response::MODEL_TARGET); }); -App::get('/v1/users') +Http::get('/v1/users') ->desc('List users') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -594,7 +594,7 @@ App::get('/v1/users') ]), Response::MODEL_USER_LIST); }); -App::get('/v1/users/:userId') +Http::get('/v1/users/:userId') ->desc('Get user') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -619,7 +619,7 @@ App::get('/v1/users/:userId') $response->dynamic($user, Response::MODEL_USER); }); -App::get('/v1/users/:userId/prefs') +Http::get('/v1/users/:userId/prefs') ->desc('Get user preferences') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -646,7 +646,7 @@ App::get('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::get('/v1/users/:userId/targets/:targetId') +Http::get('/v1/users/:userId/targets/:targetId') ->desc('Get user target') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -678,7 +678,7 @@ App::get('/v1/users/:userId/targets/:targetId') $response->dynamic($target, Response::MODEL_TARGET); }); -App::get('/v1/users/:userId/sessions') +Http::get('/v1/users/:userId/sessions') ->desc('List user sessions') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -719,7 +719,7 @@ App::get('/v1/users/:userId/sessions') ]), Response::MODEL_SESSION_LIST); }); -App::get('/v1/users/:userId/memberships') +Http::get('/v1/users/:userId/memberships') ->desc('List user memberships') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -758,7 +758,7 @@ App::get('/v1/users/:userId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -App::get('/v1/users/:userId/logs') +Http::get('/v1/users/:userId/logs') ->desc('List user logs') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -775,7 +775,8 @@ App::get('/v1/users/:userId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->inject('authorization') + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { $user = $dbForProject->getDocument('users', $userId); @@ -847,7 +848,7 @@ App::get('/v1/users/:userId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/users/:userId/targets') +Http::get('/v1/users/:userId/targets') ->desc('List user targets') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -902,7 +903,7 @@ App::get('/v1/users/:userId/targets') ]), Response::MODEL_TARGET_LIST); }); -App::get('/v1/users/identities') +Http::get('/v1/users/identities') ->desc('List identities') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -956,7 +957,7 @@ App::get('/v1/users/identities') ]), Response::MODEL_IDENTITY_LIST); }); -App::patch('/v1/users/:userId/status') +Http::patch('/v1/users/:userId/status') ->desc('Update user status') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.status') @@ -992,7 +993,7 @@ App::patch('/v1/users/:userId/status') $response->dynamic($user, Response::MODEL_USER); }); -App::put('/v1/users/:userId/labels') +Http::put('/v1/users/:userId/labels') ->desc('Update user labels') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.labels') @@ -1029,7 +1030,7 @@ App::put('/v1/users/:userId/labels') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/verification/phone') +Http::patch('/v1/users/:userId/verification/phone') ->desc('Update phone verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1064,7 +1065,7 @@ App::patch('/v1/users/:userId/verification/phone') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/name') +Http::patch('/v1/users/:userId/name') ->desc('Update name') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.name') @@ -1101,7 +1102,7 @@ App::patch('/v1/users/:userId/name') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/password') +Http::patch('/v1/users/:userId/password') ->desc('Update password') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.password') @@ -1178,7 +1179,7 @@ App::patch('/v1/users/:userId/password') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/email') +Http::patch('/v1/users/:userId/email') ->desc('Update email') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.email') @@ -1273,7 +1274,7 @@ App::patch('/v1/users/:userId/email') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/phone') +Http::patch('/v1/users/:userId/phone') ->desc('Update phone') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.phone') @@ -1356,7 +1357,7 @@ App::patch('/v1/users/:userId/phone') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/verification') +Http::patch('/v1/users/:userId/verification') ->desc('Update email verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1391,7 +1392,7 @@ App::patch('/v1/users/:userId/verification') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/prefs') +Http::patch('/v1/users/:userId/prefs') ->desc('Update user preferences') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.prefs') @@ -1424,7 +1425,7 @@ App::patch('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::patch('/v1/users/:userId/targets/:targetId') +Http::patch('/v1/users/:userId/targets/:targetId') ->desc('Update user target') ->groups(['api', 'users']) ->label('audits.event', 'target.update') @@ -1518,7 +1519,7 @@ App::patch('/v1/users/:userId/targets/:targetId') ->dynamic($target, Response::MODEL_TARGET); }); -App::patch('/v1/users/:userId/mfa') +Http::patch('/v1/users/:userId/mfa') ->desc('Update MFA') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa') @@ -1556,7 +1557,7 @@ App::patch('/v1/users/:userId/mfa') $response->dynamic($user, Response::MODEL_USER); }); -App::get('/v1/users/:userId/mfa/factors') +Http::get('/v1/users/:userId/mfa/factors') ->desc('List factors') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1589,7 +1590,7 @@ App::get('/v1/users/:userId/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -App::get('/v1/users/:userId/mfa/recovery-codes') +Http::get('/v1/users/:userId/mfa/recovery-codes') ->desc('Get MFA recovery codes') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1624,7 +1625,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::patch('/v1/users/:userId/mfa/recovery-codes') +Http::patch('/v1/users/:userId/mfa/recovery-codes') ->desc('Create MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].create.mfa.recovery-codes') @@ -1670,7 +1671,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::put('/v1/users/:userId/mfa/recovery-codes') +Http::put('/v1/users/:userId/mfa/recovery-codes') ->desc('Regenerate MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa.recovery-codes') @@ -1715,7 +1716,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -App::delete('/v1/users/:userId/mfa/authenticators/:type') +Http::delete('/v1/users/:userId/mfa/authenticators/:type') ->desc('Delete authenticator') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete.mfa') @@ -1757,7 +1758,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type') $response->noContent(); }); -App::post('/v1/users/:userId/sessions') +Http::post('/v1/users/:userId/sessions') ->desc('Create session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -1827,7 +1828,7 @@ App::post('/v1/users/:userId/sessions') ->dynamic($session, Response::MODEL_SESSION); }); -App::post('/v1/users/:userId/tokens') +Http::post('/v1/users/:userId/tokens') ->desc('Create token') ->groups(['api', 'users']) ->label('event', 'users.[userId].tokens.[tokenId].create') @@ -1884,7 +1885,7 @@ App::post('/v1/users/:userId/tokens') ->dynamic($token, Response::MODEL_TOKEN); }); -App::delete('/v1/users/:userId/sessions/:sessionId') +Http::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete user session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].delete') @@ -1927,7 +1928,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId') $response->noContent(); }); -App::delete('/v1/users/:userId/sessions') +Http::delete('/v1/users/:userId/sessions') ->desc('Delete user sessions') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.delete') @@ -1969,7 +1970,7 @@ App::delete('/v1/users/:userId/sessions') $response->noContent(); }); -App::delete('/v1/users/:userId') +Http::delete('/v1/users/:userId') ->desc('Delete user') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete') @@ -2011,7 +2012,7 @@ App::delete('/v1/users/:userId') $response->noContent(); }); -App::delete('/v1/users/:userId/targets/:targetId') +Http::delete('/v1/users/:userId/targets/:targetId') ->desc('Delete user target') ->groups(['api', 'users']) ->label('audits.event', 'target.delete') @@ -2062,7 +2063,7 @@ App::delete('/v1/users/:userId/targets/:targetId') $response->noContent(); }); -App::delete('/v1/users/identities/:identityId') +Http::delete('/v1/users/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'users']) ->label('event', 'users.[userId].identities.[identityId].delete') @@ -2097,7 +2098,7 @@ App::delete('/v1/users/identities/:identityId') return $response->noContent(); }); -App::post('/v1/users/:userId/jwts') +Http::post('/v1/users/:userId/jwts') ->desc('Create user JWT') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -2147,7 +2148,7 @@ App::post('/v1/users/:userId/jwts') ])]), Response::MODEL_JWT); }); -App::get('/v1/users/usage') +Http::get('/v1/users/usage') ->desc('Get users usage stats') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -2160,8 +2161,8 @@ App::get('/v1/users/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('register') - ->action(function (string $range, Response $response, Database $dbForProject) { + ->inject('authorization') + ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -2171,7 +2172,7 @@ App::get('/v1/users/usage') METRIC_SESSIONS, ]; - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $count => $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index f3381490ec..987d84bb7e 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -8,7 +8,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -32,17 +31,17 @@ use Utopia\Detector\Adapter\Python; use Utopia\Detector\Adapter\Ruby; use Utopia\Detector\Adapter\Swift; use Utopia\Detector\Detector; +use Utopia\Http\Http; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\Host; -use Utopia\Validator\Text; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; use function Swoole\Coroutine\batch; -$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) { - $errors = []; +$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request, Authorization $auth) { foreach ($repositories as $resource) { try { $resourceType = $resource->getAttribute('resourceType'); @@ -52,11 +51,11 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } $projectId = $resource->getAttribute('projectId'); - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); $functionId = $resource->getAttribute('resourceId'); - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); $functionInternalId = $function->getInternalId(); $deploymentId = ID::unique(); @@ -102,8 +101,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = ''; - if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) { - $latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [ + if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false)) { + $latestComment = $auth->skip(fn () => $dbForConsole->findOne('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerPullRequestId', [$providerPullRequestId]), Query::orderDesc('$createdAt'), @@ -124,7 +123,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId if (!empty($latestCommentId)) { $teamId = $project->getAttribute('teamId', ''); - $latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ + $latestComment = $auth->skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::team(ID::custom($teamId))), @@ -145,7 +144,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } } } elseif (!empty($providerBranch)) { - $latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [ + $latestComments = $auth->skip(fn () => $dbForConsole->find('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerBranch', [$providerBranch]), Query::orderDesc('$createdAt'), @@ -262,8 +261,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } }; -App::get('/v1/vcs/github/authorize') - ->desc('Install GitHub app') +Http::get('/v1/vcs/github/authorize') + ->desc('Install GitHub App') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -304,8 +303,8 @@ App::get('/v1/vcs/github/authorize') ->redirect($url); }); -App::get('/v1/vcs/github/callback') - ->desc('Capture installation and authorization from GitHub app') +Http::get('/v1/vcs/github/callback') + ->desc('Capture installation and authorization from GitHub App') ->groups(['api', 'vcs']) ->label('scope', 'public') ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -464,7 +463,7 @@ App::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') ->desc('Get files and directories of a VCS repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -525,7 +524,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro ]), Response::MODEL_VCS_CONTENT_LIST); }); -App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') +Http::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') ->desc('Detect runtime settings from source code') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -597,8 +596,8 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr $response->dynamic(new Document($detection), Response::MODEL_DETECTION); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('List repositories') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('List Repositories') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -692,7 +691,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') ]), Response::MODEL_PROVIDER_REPOSITORY_LIST); }); -App::post('/v1/vcs/github/installations/:installationId/providerRepositories') +Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') ->desc('Create repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -793,7 +792,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories') $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') ->desc('Get repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -842,8 +841,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') - ->desc('List repository branches') +Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') + ->desc('List Repository Branches') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -891,8 +890,8 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro ]), Response::MODEL_BRANCH_LIST); }); -App::post('/v1/vcs/github/events') - ->desc('Create event') +Http::post('/v1/vcs/github/events') + ->desc('Create Event') ->groups(['api', 'vcs']) ->label('scope', 'public') ->inject('gitHub') @@ -901,8 +900,9 @@ App::post('/v1/vcs/github/events') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') + ->inject('auth') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -936,14 +936,14 @@ App::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -956,13 +956,13 @@ App::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); + $auth->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -994,12 +994,12 @@ App::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1008,7 +1008,7 @@ App::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1019,7 +1019,7 @@ App::post('/v1/vcs/github/events') if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1030,7 +1030,7 @@ App::post('/v1/vcs/github/events') } ); -App::get('/v1/vcs/installations') +Http::get('/v1/vcs/installations') ->desc('List installations') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1090,7 +1090,7 @@ App::get('/v1/vcs/installations') ]), Response::MODEL_INSTALLATION_LIST); }); -App::get('/v1/vcs/installations/:installationId') +Http::get('/v1/vcs/installations/:installationId') ->desc('Get installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1119,8 +1119,8 @@ App::get('/v1/vcs/installations/:installationId') $response->dynamic($installation, Response::MODEL_INSTALLATION); }); -App::delete('/v1/vcs/installations/:installationId') - ->desc('Delete installation') +Http::delete('/v1/vcs/installations/:installationId') + ->desc('Delete Installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') ->label('sdk.namespace', 'vcs') @@ -1152,7 +1152,7 @@ App::delete('/v1/vcs/installations/:installationId') $response->noContent(); }); -App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') +Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') ->desc('Authorize external deployment') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -1172,14 +1172,15 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { + ->inject('auth') + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = $auth->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) ])); @@ -1196,7 +1197,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor // TODO: Delete from array when PR is closed - $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1220,7 +1221,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); $response->noContent(); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 04554a940e..1e403ef93a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1,7 +1,5 @@ getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); + $route?->label('error', __DIR__ . '/../views/general/error.phtml'); $host = $request->getHostname() ?? ''; - $route = Authorization::skip( + $rule = $auth->skip( fn () => $dbForConsole->find('rules', [ Query::equal('domain', [$host]), Query::limit(1) ]) )[0] ?? null; - if ($route === null) { + if ($rule === null) { if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) { throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.'); } @@ -73,12 +72,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } // Act as API - no Proxy logic - $utopia->getRoute()?->label('error', ''); + $route?->label('error', ''); return false; } - $projectId = $route->getAttribute('projectId'); - $project = Authorization::skip( + $projectId = $rule->getAttribute('projectId'); + $project = $auth->skip( fn () => $dbForConsole->getDocument('projects', $projectId) ); if (array_key_exists('proxy', $project->getAttribute('services', []))) { @@ -89,16 +88,16 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } // Skip Appwrite Router for ACME challenge. Nessessary for certificate generation - $path = ($swooleRequest->server['request_uri'] ?? '/'); + $path = ($request->getURI() ?? '/'); if (\str_starts_with($path, '/.well-known/acme-challenge')) { return false; } - $type = $route->getAttribute('resourceType'); + $type = $rule->getAttribute('resourceType'); if ($type === 'function') { - $utopia->getRoute()?->label('sdk.namespace', 'functions'); - $utopia->getRoute()?->label('sdk.method', 'createExecution'); + $route->label('sdk.namespace', 'functions'); + $route->label('sdk.method', 'createExecution'); if (System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if ($request->getProtocol() !== 'https') { @@ -110,26 +109,25 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } } - $functionId = $route->getAttribute('resourceId'); - $projectId = $route->getAttribute('projectId'); + $functionId = $rule->getAttribute('resourceId'); + $projectId = $rule->getAttribute('projectId'); - $path = ($swooleRequest->server['request_uri'] ?? '/'); - $query = ($swooleRequest->server['query_string'] ?? ''); + $path = ($request->getURI() ?? '/'); + $query = ($request->getQueryString() ?? ''); if (!empty($query)) { $path .= '?' . $query; } - - $body = $swooleRequest->getContent() ?? ''; - $method = $swooleRequest->server['request_method']; + $body = $request->getRawPayload() ?? ''; + $method = $request->getMethod(); $requestHeaders = $request->getHeaders(); - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); if ($function->isEmpty() || !$function->getAttribute('enabled')) { throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); @@ -145,7 +143,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = $auth->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -156,7 +154,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = $auth->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); } @@ -217,7 +215,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo 'deploymentInternalId' => $deployment->getInternalId(), 'deploymentId' => $deployment->getId(), 'trigger' => 'http', // http / schedule / event - 'status' => 'processing', // waiting / processing / completed / failed + 'status' => 'processing', // waiting / processing / completed / failed 'responseStatusCode' => 0, 'responseHeaders' => [], 'requestPath' => $path, @@ -313,7 +311,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, logging: $function->getAttribute('logging', true), - requestTimeout: 30 ); $headersFiltered = []; @@ -331,7 +328,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('duration', $executionResponse['duration']); - } catch (\Throwable $th) { $durationEnd = \microtime(true); @@ -366,7 +362,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo ->trigger() ; - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + /** @var Document $execution */ + $execution = $auth->skip(fn () => $dbForProject->createDocument('executions', $execution)); } $execution->setAttribute('logs', ''); @@ -398,18 +395,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo return true; } elseif ($type === 'api') { - $utopia->getRoute()?->label('error', ''); + $route?->label('error', ''); return false; } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); } - - $utopia->getRoute()?->label('error', ''); - return false; } /* -App::init() +Http::init() ->groups(['api']) ->inject('project') ->inject('mode') @@ -420,7 +414,7 @@ App::init() }); */ -App::init() +Http::init() ->groups(['database', 'functions', 'storage', 'messaging']) ->inject('project') ->inject('request') @@ -433,12 +427,11 @@ App::init() } }); -App::init() +Http::init() ->groups(['api', 'web']) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') + ->inject('route') ->inject('console') ->inject('project') ->inject('dbForConsole') @@ -450,7 +443,27 @@ App::init() ->inject('queueForUsage') ->inject('queueForEvents') ->inject('queueForCertificates') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates) { + ->inject('authorization') + ->action(function ( + Request $request, + Response $response, + Route $route, + Document $console, + Document $project, + Database $dbForConsole, + $getProjectDB, + Locale $locale, + array $localeCodes, + array $clients, + /** + * @disregard P1009 Undefined type + */ + Reader $geodb, + Usage $queueForUsage, + Event $queueForEvents, + Certificate $queueForCertificates, + Authorization $authorization + ) { /* * Appwrite Router */ @@ -458,7 +471,7 @@ App::init() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { return; } } @@ -466,8 +479,8 @@ App::init() /* * Request format */ - $route = $utopia->getRoute(); - Request::setRoute($route); + //$route = $utopia->getRoute(); + //Request::setRoute($route); if ($route === null) { return $response @@ -499,7 +512,7 @@ App::init() } elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { Console::warning('Skipping SSL certificates generation on ACME challenge.'); } else { - Authorization::disable(); + $authorization->disable(); $envDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = null; @@ -538,12 +551,12 @@ App::init() } $domains[$domain->get()] = true; - Authorization::reset(); // ensure authorization is re-enabled + $authorization->reset(); // ensure authorization is re-enabled } Config::setParam('domains', $domains); } - $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); + $localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); if (\in_array($localeParam, $localeCodes)) { $locale->setDefault($localeParam); } @@ -571,7 +584,7 @@ App::init() Config::setParam( 'domainVerification', ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' + $endDomain->getRegisterable() !== '' ); $isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort(); @@ -586,8 +599,8 @@ App::init() ? null : ( $isConsoleProject && $isConsoleRootSession - ? '.' . $selfDomain->getRegisterable() - : '.' . $request->getHostname() + ? '.' . $selfDomain->getRegisterable() + : '.' . $request->getHostname() ) ); @@ -606,7 +619,7 @@ App::init() $response->addFilter(new ResponseV18()); } if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) { - $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); + $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); } } @@ -617,7 +630,9 @@ App::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + if ($request->getProtocol() !== 'https' // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + && ($request->getHeader('host') ?? '') !== 'localhost' + && ($request->getHeader('host') ?? '') !== APP_HOSTNAME_INTERNAL) { if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } @@ -657,9 +672,8 @@ App::init() } }); -App::options() - ->inject('utopia') - ->inject('swooleRequest') +Http::options() + ->inject('route') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -667,7 +681,8 @@ App::options() ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('authorization') + ->action(function (Route $route, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Authorization $authorization) { /* * Appwrite Router */ @@ -675,7 +690,7 @@ App::options() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { return; } } @@ -692,18 +707,25 @@ App::options() ->noContent(); }); -App::error() +Http::error() ->inject('error') - ->inject('utopia') + ->inject('user') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') ->inject('logger') ->inject('log') + ->inject('authorization') + ->inject('connections') ->inject('queueForUsage') - ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) { + ->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections, Usage $queueForUsage) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $route = $utopia->getRoute(); + + if (is_null($route)) { + $route = new Route($request->getMethod(), $request->getURI()); + } + $class = \get_class($error); $code = $error->getCode(); $message = $error->getMessage(); @@ -724,9 +746,9 @@ App::error() Console::error('[Error] File: ' . $file); Console::error('[Error] Line: ' . $line); } - switch ($class) { - case 'Utopia\Exception': + case 'Utopia\Servers\Exception': + case 'Utopia\Http\Exception': $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); switch ($code) { case 400: @@ -771,35 +793,36 @@ App::error() } else { $publish = $error->getCode() === 0 || $error->getCode() >= 500; } - if ($error->getCode() >= 400 && $error->getCode() < 500) { // Register error logger $providerName = System::getEnv('_APP_EXPERIMENT_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_EXPERIMENT_LOGGING_CONFIG', ''); - try { - $loggingProvider = new DSN($providerConfig ?? ''); - $providerName = $loggingProvider->getScheme(); + if (!(empty($providerName) || empty($providerConfig))) { + try { + $loggingProvider = new DSN($providerConfig); + $providerName = $loggingProvider->getScheme(); - if (!empty($providerName) && $providerName === 'sentry') { - $key = $loggingProvider->getPassword(); - $projectId = $loggingProvider->getUser() ?? ''; - $host = 'https://' . $loggingProvider->getHost(); + if (!empty($providerName) && $providerName === 'sentry') { + $key = $loggingProvider->getPassword(); + $projectId = $loggingProvider->getUser() ?? ''; + $host = 'https://' . $loggingProvider->getHost(); - $adapter = new Sentry($projectId, $key, $host); - $logger = new Logger($adapter); - $logger->setSample(0.04); - $publish = true; - } else { - throw new \Exception('Invalid experimental logging provider'); + $adapter = new Sentry($projectId, $key, $host); + $logger = new Logger($adapter); + $logger->setSample(0.04); + $publish = true; + } else { + throw new \Exception('Invalid experimental logging provider'); + } + } catch (\Throwable $th) { + Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); } - } catch (\Throwable $th) { - Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); } } if ($publish && $project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!Auth::isPrivilegedUser($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -818,14 +841,7 @@ App::error() } - if ($logger && $publish) { - try { - /** @var Utopia\Database\Document $user */ - $user = $utopia->getResource('user'); - } catch (\Throwable) { - // All good, user is optional information for logger - } - + if ($logger && ($publish || $error->getCode() === 0)) { if (isset($user) && !$user->isEmpty()) { $log->setUser(new User($user->getId())); } @@ -855,7 +871,7 @@ App::error() $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); + $log->addExtra('roles', $authorization->getRoles()); $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); $log->setAction($action); @@ -873,7 +889,7 @@ App::error() /** Wrap all exceptions inside Appwrite\Extend\Exception */ if (!($error instanceof AppwriteException)) { - $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); + $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error); } switch ($code) { // Don't show 500 errors! @@ -893,14 +909,14 @@ App::error() break; default: $code = 500; // All other errors get the generic 500 server error status code - $message = 'Server Error'; + $message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error'; } //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised $type = $error->getType(); - $output = ((App::isDevelopment())) ? [ + $output = ((Http::isDevelopment())) ? [ 'message' => $message, 'code' => $code, 'file' => $file, @@ -928,7 +944,7 @@ App::error() $layout ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('development', App::isDevelopment()) + ->setParam('development', Http::isDevelopment()) ->setParam('projectName', $project->getAttribute('name')) ->setParam('projectURL', $project->getAttribute('url')) ->setParam('message', $output['message'] ?? '') @@ -939,18 +955,18 @@ App::error() $response->html($layout->render()); } + $connections->reclaim(); + $response->dynamic( new Document($output), - $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR ); }); -App::get('/robots.txt') +Http::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') ->label('docs', false) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -958,7 +974,9 @@ App::get('/robots.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('route') + ->inject('authorization') + ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, ?Route $route, Authorization $authorization) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -966,16 +984,17 @@ App::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); + if (is_null($route)) { + $route = new Route($request->getMethod(), $request->getURI()); + } + router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); } }); -App::get('/humans.txt') +Http::get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) - ->inject('utopia') - ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -983,7 +1002,9 @@ App::get('/humans.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->inject('route') + ->inject('authorization') + ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Route $route, Authorization $authorization) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -991,11 +1012,11 @@ App::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); + router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); } }); -App::get('/.well-known/acme-challenge/*') +Http::get('/.well-known/acme-challenge/*') ->desc('SSL Verification') ->label('scope', 'public') ->label('docs', false) @@ -1045,16 +1066,32 @@ App::get('/.well-known/acme-challenge/*') $response->text($content); }); -include_once __DIR__ . '/shared/api.php'; -include_once __DIR__ . '/shared/api/auth.php'; - -App::wildcard() +Http::wildcard() ->groups(['api']) ->label('scope', 'global') ->action(function () { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); -foreach (Config::getParam('services', []) as $service) { - include_once $service['controller']; -} +include_once 'mock.php'; +include_once 'shared/api.php'; +include_once 'shared/api/auth.php'; +include_once 'api/account.php'; +include_once 'api/avatars.php'; +include_once 'api/console.php'; +include_once 'api/databases.php'; +include_once 'api/functions.php'; +include_once 'api/graphql.php'; +include_once 'api/health.php'; +include_once 'api/locale.php'; +include_once 'api/messaging.php'; +include_once 'api/migrations.php'; +include_once 'api/project.php'; +include_once 'api/projects.php'; +include_once 'api/proxy.php'; +include_once 'api/storage.php'; +include_once 'api/teams.php'; +include_once 'api/users.php'; +include_once 'api/vcs.php'; +include_once 'web/console.php'; +include_once 'web/home.php'; diff --git a/app/controllers/mock.php b/app/controllers/mock.php index fdb1d80dcc..70ca5e9048 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -3,9 +3,7 @@ global $utopia, $request, $response; use Appwrite\Extend\Exception; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -13,13 +11,15 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; +use Utopia\Http\Http; +use Utopia\Http\Route; +use Utopia\Http\Validator\Host; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\System\System; -use Utopia\Validator\Host; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; -App::get('/v1/mock/tests/general/oauth2') +Http::get('/v1/mock/tests/general/oauth2') ->desc('OAuth Login') ->groups(['mock']) ->label('scope', 'public') @@ -35,7 +35,7 @@ App::get('/v1/mock/tests/general/oauth2') $response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state])); }); -App::get('/v1/mock/tests/general/oauth2/token') +Http::get('/v1/mock/tests/general/oauth2/token') ->desc('OAuth2 Token') ->groups(['mock']) ->label('scope', 'public') @@ -81,7 +81,7 @@ App::get('/v1/mock/tests/general/oauth2/token') } }); -App::get('/v1/mock/tests/general/oauth2/user') +Http::get('/v1/mock/tests/general/oauth2/user') ->desc('OAuth2 User') ->groups(['mock']) ->label('scope', 'public') @@ -101,7 +101,7 @@ App::get('/v1/mock/tests/general/oauth2/user') ]); }); -App::get('/v1/mock/tests/general/oauth2/success') +Http::get('/v1/mock/tests/general/oauth2/success') ->desc('OAuth2 Success') ->groups(['mock']) ->label('scope', 'public') @@ -114,7 +114,7 @@ App::get('/v1/mock/tests/general/oauth2/success') ]); }); -App::get('/v1/mock/tests/general/oauth2/failure') +Http::get('/v1/mock/tests/general/oauth2/failure') ->desc('OAuth2 Failure') ->groups(['mock']) ->label('scope', 'public') @@ -129,7 +129,7 @@ App::get('/v1/mock/tests/general/oauth2/failure') ]); }); -App::patch('/v1/mock/functions-v2') +Http::patch('/v1/mock/functions-v2') ->desc('Update Function Version to V2 (outdated code syntax)') ->groups(['mock', 'api', 'functions']) ->label('scope', 'functions.write') @@ -155,7 +155,7 @@ App::patch('/v1/mock/functions-v2') $response->noContent(); }); -App::post('/v1/mock/api-key-unprefixed') +Http::post('/v1/mock/api-key-unprefixed') ->desc('Create API Key (without standard prefix)') ->groups(['mock', 'api', 'projects']) ->label('scope', 'projects.write') @@ -204,7 +204,7 @@ App::post('/v1/mock/api-key-unprefixed') ->dynamic($key, Response::MODEL_KEY); }); -App::get('/v1/mock/github/callback') +Http::get('/v1/mock/github/callback') ->desc('Create installation document using GitHub installation id') ->groups(['mock', 'api', 'vcs']) ->label('scope', 'public') @@ -264,15 +264,13 @@ App::get('/v1/mock/github/callback') ]); }); -App::shutdown() +Http::shutdown() ->groups(['mock']) - ->inject('utopia') + ->inject('route') ->inject('response') - ->inject('request') - ->action(function (App $utopia, Response $response, Request $request) { + ->action(function (Route $route, Response $response) { $result = []; - $route = $utopia->getRoute(); $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 357d73adc8..f35643dad5 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -15,11 +15,11 @@ use Appwrite\Event\Usage; use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; use Utopia\Cache\Adapter\Filesystem; use Utopia\Cache\Cache; use Utopia\Config\Config; @@ -28,8 +28,11 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Authorization\Input; +use Utopia\Http\Http; +use Utopia\Http\Route; +use Utopia\Http\Validator\WhiteList; use Utopia\System\System; -use Utopia\Validator\WhiteList; $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { preg_match_all('/{(.*?)}/', $label, $matches); @@ -150,9 +153,9 @@ $databaseListener = function (string $event, Document $document, Document $proje } }; -App::init() +Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('dbForConsole') ->inject('project') @@ -160,9 +163,8 @@ App::init() ->inject('session') ->inject('servers') ->inject('mode') - ->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode) { - $route = $utopia->getRoute(); - + ->inject('authorization') + ->action(function (Route $route, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Authorization $authorization) { if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } @@ -241,8 +243,8 @@ App::init() $role = Auth::USER_ROLE_APPS; $scopes = \array_merge($roles[$role]['scopes'], $tokenScopes); - Authorization::setRole(Auth::USER_ROLE_APPS); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. + $authorization->addRole(Auth::USER_ROLE_APPS); + $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. } } elseif ($keyType === API_KEY_STANDARD) { // No underline means no prefix. Backwards compatibility. @@ -267,8 +269,8 @@ App::init() throw new Exception(Exception::PROJECT_KEY_EXPIRED); } - Authorization::setRole(Auth::USER_ROLE_APPS); - Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. + $authorization->addRole(Auth::USER_ROLE_APPS); + $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. $accessedAt = $key->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { @@ -295,10 +297,10 @@ App::init() } } - Authorization::setRole($role); + $authorization->addRole($role); - foreach (Auth::getRoles($user) as $authRole) { - Authorization::setRole($authRole); + foreach (Auth::getRoles($user, $authorization) as $authRole) { + $authorization->addRole($authRole); } $service = $route->getLabel('sdk.namespace', ''); @@ -306,7 +308,7 @@ App::init() if ( array_key_exists($service, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -341,9 +343,9 @@ App::init() } }); -App::init() +Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -357,14 +359,12 @@ App::init() ->inject('queueForUsage') ->inject('dbForProject') ->inject('mode') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) { - - $route = $utopia->getRoute(); - + ->inject('authorization') + ->action(function (Route $route, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode, Authorization $authorization) use ($databaseListener) { if ( array_key_exists('rest', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['rest'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -394,7 +394,7 @@ App::init() $closestLimit = null; - $roles = Authorization::getRoles(); + $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -458,7 +458,7 @@ App::init() $useCache = $route->getLabel('cache', false); if ($useCache) { $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) ); @@ -471,19 +471,18 @@ App::init() if ($type === 'bucket') { $bucketId = $parts[1] ?? null; - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + + $isAPIKey = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization(Database::PERMISSION_READ); - $valid = $validator->isValid($bucket->getRead()); - + $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -494,7 +493,7 @@ App::init() if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -518,7 +517,7 @@ App::init() } }); -App::init() +Http::init() ->groups(['session']) ->inject('user') ->inject('request') @@ -538,14 +537,12 @@ App::init() * Delete older sessions if the number of sessions have crossed * the session limit set for the project */ -App::shutdown() +Http::shutdown() ->groups(['session']) - ->inject('utopia') - ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Database $dbForProject) { + ->action(function (Response $response, Document $project, Database $dbForProject) { $sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT; $session = $response->getPayload(); $userId = $session['userId'] ?? ''; @@ -572,9 +569,9 @@ App::shutdown() $dbForProject->purgeCachedDocument('users', $userId); }); -App::shutdown() +Http::shutdown() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -590,7 +587,29 @@ App::shutdown() ->inject('queueForFunctions') ->inject('mode') ->inject('dbForConsole') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) { + ->inject('authorization') + ->action(function ( + Route $route, + Request $request, + Response $response, + Document $project, + Document $user, + Event $queueForEvents, + Audit $queueForAudits, + Usage $queueForUsage, + Delete $queueForDeletes, + EventDatabase $queueForDatabase, + Build $queueForBuilds, + Messaging $queueForMessaging, + Database $dbForProject, + Func $queueForFunctions, + string $mode, + Database $dbForConsole, + Authorization $authorization, + ) use ($parseLabel) { + if (!empty($user) && !$user->isEmpty() && empty($user->getInternalId())) { + $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $user->getId())); + } $responsePayload = $response->getPayload(); @@ -650,7 +669,6 @@ App::shutdown() } } - $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); /** @@ -720,11 +738,11 @@ App::shutdown() $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); $signature = md5($data['payload']); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', ''); $now = DateTime::now(); if ($cacheLog->isEmpty()) { - Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ + $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([ '$id' => $key, 'resource' => $resource, 'resourceType' => $resourceType, @@ -734,7 +752,7 @@ App::shutdown() ]))); } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); - Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); } if ($signature !== $cacheLog->getAttribute('signature')) { @@ -746,10 +764,8 @@ App::shutdown() } } - - if ($project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!Auth::isPrivilegedUser($authorization->getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -774,7 +790,7 @@ App::shutdown() $accessedAt = $project->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); } } @@ -795,10 +811,16 @@ App::shutdown() } }); -App::init() +Http::init() ->groups(['usage']) ->action(function () { if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') { throw new Exception(Exception::GENERAL_USAGE_DISABLED); } }); + +Http::shutdown() + ->inject('connections') + ->action(function (Connections $connections) { + $connections->reclaim(); + }); diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index 53aacabe21..224dcb3392 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -4,13 +4,14 @@ use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Utopia\Request; use MaxMind\Db\Reader; -use Utopia\App; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\System\System; -App::init() +Http::init() ->groups(['mfaProtected']) ->inject('session') ->action(function (Document $session) { @@ -29,13 +30,14 @@ App::init() } }); -App::init() +Http::init() ->groups(['auth']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('project') ->inject('geodb') - ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { + ->inject('authorization') + ->action(function (Route $route, Request $request, Document $project, Reader $geodb, Authorization $authorization) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -46,15 +48,17 @@ App::init() } } - $route = $utopia->match($request); - - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAppUser = Auth::isAppUser($authorization->getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; } + if ($route->getLabel('sdk.namespace', '') === 'graphql') { // Skip for graphQL recursive call + return; + } + $auths = $project->getAttribute('auths', []); switch ($route->getLabel('auth.type', '')) { case 'emailPassword': diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index c02e140270..60be4acf54 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -2,9 +2,9 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; +use Utopia\Http\Http; -App::init() +Http::init() ->groups(['web']) ->inject('request') ->inject('response') @@ -16,7 +16,7 @@ App::init() ; }); -App::get('/') +Http::get('/') ->alias('auth/*') ->alias('/invite') ->alias('/login') diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 27b2614c37..3577061d69 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,10 +1,10 @@ desc('Get Version') ->groups(['home', 'web']) ->label('scope', 'public') diff --git a/app/http.php b/app/http.php index bec772c770..320621ad33 100644 --- a/app/http.php +++ b/app/http.php @@ -1,335 +1,242 @@ set([ - 'worker_num' => $workerNumber, - 'open_http2_protocol' => true, - 'http_compression' => true, - 'http_compression_level' => 6, - 'package_max_length' => $payloadSize, - 'buffer_output_size' => $payloadSize, - ]); +$server = new Server('0.0.0.0', '80', [ + 'open_http2_protocol' => true, + 'http_compression' => true, + 'http_compression_level' => 6, + 'package_max_length' => $payloadSize, + 'buffer_output_size' => $payloadSize, + // Server + // 'log_level' => 0, + 'dispatch_mode' => 2, + 'worker_num' => $workerNumber, + 'reactor_num' => swoole_cpu_num() * 2, + 'open_cpu_affinity' => true, + // Coroutine + 'enable_coroutine' => true, + 'send_yield' => true, + 'tcp_fastopen' => true, +]); -$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) { - Console::success('Worker ' . ++$workerId . ' started successfully'); -}); - -$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server, $workerId) { - Console::success('Starting reload...'); -}); - -$http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) { - Console::success('Reload completed...'); -}); - -include __DIR__ . '/controllers/general.php'; - -$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) { - $app = new App('UTC'); - - go(function () use ($register, $app) { - $pools = $register->get('pools'); - /** @var Group $pools */ - App::setResource('pools', fn () => $pools); - - // wait for database to be ready - $attempts = 0; - $max = 10; - $sleep = 1; - - do { - try { - $attempts++; - $dbForConsole = $app->getResource('dbForConsole'); - /** @var Utopia\Database\Database $dbForConsole */ - break; // leave the do-while if successful - } catch (\Throwable $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: ' . $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - Console::success('[Setup] - Server database init started...'); +$http = new Http($server, $container, 'UTC'); +$http->setRequestClass(Request::class); +$http->setResponseClass(Response::class); +Http::onStart() + ->inject('authorization') + ->inject('cache') + ->inject('pools') + ->inject('connections') + ->action(function (Authorization $authorization, Cache $cache, array $pools, Connections $connections) { try { - Console::success('[Setup] - Creating database: appwrite...'); - $dbForConsole->create(); + // wait for database to be ready + $attempts = 0; + $max = 15; + $sleep = 2; + + do { + try { + $attempts++; + $pool = $pools['pools-console-console']['pool']; + $dsn = $pools['pools-console-console']['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); + + $dbForConsole = new Database($adapter, $cache); + $dbForConsole->setAuthorization($authorization); + + $dbForConsole + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + $dbForConsole->ping(); + break; // leave the do-while if successful + } catch (\Throwable $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: ' . $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + Console::success('[Setup] - Server database init started...'); + + try { + Console::success('[Setup] - Creating database: appwrite...'); + $dbForConsole->create(); + } catch (\Throwable $e) { + Console::success('[Setup] - Skip: metadata table already exists'); + return true; + } + + if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { + $audit = new Audit($dbForConsole); + $audit->setup(); + } + + if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { + $abuse = new TimeLimit("", 0, 1, $dbForConsole); + $abuse->setup(); + } + + /** @var array $collections */ + $collections = Config::getParam('collections', []); + $consoleCollections = $collections['console']; + foreach ($consoleCollections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + if (!$dbForConsole->getCollection($key)->isEmpty()) { + continue; + } + + Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); + + $attributes = []; + $indexes = []; + + foreach ($collection['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($collection['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection($key, $attributes, $indexes); + } + + if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { + Console::success('[Setup] - Creating default bucket...'); + $dbForConsole->createDocument('buckets', new Document([ + '$id' => ID::custom('default'), + '$collection' => ID::custom('buckets'), + 'name' => 'Default', + 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'allowedFileExtensions' => [], + 'enabled' => true, + 'compression' => 'gzip', + 'encryption' => true, + 'antivirus' => true, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'search' => 'buckets Default', + ])); + + $bucket = $dbForConsole->getDocument('buckets', 'default'); + + Console::success('[Setup] - Creating files collection for default bucket...'); + + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = []; + $indexes = []; + + foreach ($files['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($files['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } + + $connections->reclaim(); + + Console::success('[Setup] - Server database init completed...'); + Console::success('Server started successfully'); } catch (\Throwable $e) { - Console::success('[Setup] - Skip: metadata table already exists'); + Console::warning('Database not ready: ' . $e->getMessage()); + exit(1); } - - if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { - $audit = new Audit($dbForConsole); - $audit->setup(); - } - - if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { - $adapter = new TimeLimit("", 0, 1, $dbForConsole); - $adapter->setup(); - } - - /** @var array $collections */ - $collections = Config::getParam('collections', []); - $consoleCollections = $collections['console']; - foreach ($consoleCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForConsole->getCollection($key)->isEmpty()) { - continue; - } - - Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); - - $attributes = []; - $indexes = []; - - foreach ($collection['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($collection['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection($key, $attributes, $indexes); - } - - if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { - Console::success('[Setup] - Creating default bucket...'); - $dbForConsole->createDocument('buckets', new Document([ - '$id' => ID::custom('default'), - '$collection' => ID::custom('buckets'), - 'name' => 'Default', - 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB - 'allowedFileExtensions' => [], - 'enabled' => true, - 'compression' => 'gzip', - 'encryption' => true, - 'antivirus' => true, - 'fileSecurity' => true, - '$permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'search' => 'buckets Default', - ])); - - $bucket = $dbForConsole->getDocument('buckets', 'default'); - - Console::success('[Setup] - Creating files collection for default bucket...'); - $files = $collections['buckets']['files'] ?? []; - if (empty($files)) { - throw new Exception('Files collection is not configured.'); - } - - $attributes = []; - $indexes = []; - - foreach ($files['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($files['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); - } - - $pools->reclaim(); - - Console::success('[Setup] - Server database init completed...'); }); - Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)'); - Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}"); - - // listen ctrl + c - Process::signal(2, function () use ($http) { - Console::log('Stop by Ctrl+C'); - $http->shutdown(); +Http::init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->cleanRoles(); + $authorization->addRole(Role::any()->toString()); }); -}); - -$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) { - App::setResource('swooleRequest', fn () => $swooleRequest); - App::setResource('swooleResponse', fn () => $swooleResponse); - - $request = new Request($swooleRequest); - $response = new Response($swooleResponse); - - if (Files::isFileLoaded($request->getURI())) { - $time = (60 * 60 * 24 * 365 * 2); // 45 days cache - - $response - ->setContentType(Files::getFileMimeType($request->getURI())) - ->addHeader('Cache-Control', 'public, max-age=' . $time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache - ->send(Files::getFileContents($request->getURI())); - - return; - } - - $app = new App('UTC'); - - $pools = $register->get('pools'); - App::setResource('pools', fn () => $pools); - - try { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); - - $app->run($request, $response); - } catch (\Throwable $th) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $logger = $app->getResource("logger"); - if ($logger) { - try { - /** @var Utopia\Database\Document $user */ - $user = $app->getResource('user'); - } catch (\Throwable $_th) { - // All good, user is optional information for logger - } - - $route = $app->getRoute(); - - $log = $app->getResource("log"); - - if (isset($user) && !$user->isEmpty()) { - $log->setUser(new User($user->getId())); - } - - $log->setNamespace("http"); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($th->getMessage()); - - $log->addTag('method', $route->getMethod()); - $log->addTag('url', $route->getPath()); - $log->addTag('verboseType', get_class($th)); - $log->addTag('code', $th->getCode()); - // $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant - $log->addTag('hostname', $request->getHostname()); - $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); - - $log->addExtra('file', $th->getFile()); - $log->addExtra('line', $th->getLine()); - $log->addExtra('trace', $th->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); - - $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); - $log->setAction($action); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - try { - $responseCode = $logger->addLog($log); - Console::info('Error log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - } - } - - Console::error('[Error] Type: ' . get_class($th)); - Console::error('[Error] Message: ' . $th->getMessage()); - Console::error('[Error] File: ' . $th->getFile()); - Console::error('[Error] Line: ' . $th->getLine()); - - $swooleResponse->setStatusCode(500); - - $output = ((App::isDevelopment())) ? [ - 'message' => 'Error: ' . $th->getMessage(), - 'code' => 500, - 'file' => $th->getFile(), - 'line' => $th->getLine(), - 'trace' => $th->getTrace(), - 'version' => $version, - ] : [ - 'message' => 'Error: Server Error', - 'code' => 500, - 'version' => $version, - ]; - - $swooleResponse->end(\json_encode($output)); - } finally { - $pools->reclaim(); - } -}); $http->start(); diff --git a/app/init.php b/app/init.php index f94c9665e7..0f9e8f0ef6 100644 --- a/app/init.php +++ b/app/init.php @@ -1,26 +1,12 @@ $value], JSON_PRESERVE_ZERO_FRACTION); - }, - function (mixed $value) { - if (is_null($value)) { - return; - } +$container = new Container(); +$registry = new Registry(); - return json_decode($value, true)['value']; - } -); - -Database::addFilter( - 'enum', - function (mixed $value, Document $attribute) { - if ($attribute->isSet('elements')) { - $attribute->removeAttribute('elements'); - } - - return $value; - }, - function (mixed $value, Document $attribute) { - $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); - if (isset($formatOptions['elements'])) { - $attribute->setAttribute('elements', $formatOptions['elements']); - } - - return $value; - } -); - -Database::addFilter( - 'range', - function (mixed $value, Document $attribute) { - if ($attribute->isSet('min')) { - $attribute->removeAttribute('min'); - } - if ($attribute->isSet('max')) { - $attribute->removeAttribute('max'); - } - - return $value; - }, - function (mixed $value, Document $attribute) { - $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); - if (isset($formatOptions['min']) || isset($formatOptions['max'])) { - $attribute - ->setAttribute('min', $formatOptions['min']) - ->setAttribute('max', $formatOptions['max']); - } - - return $value; - } -); - -Database::addFilter( - 'subQueryAttributes', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - $attributes = $database->find('attributes', [ - Query::equal('collectionInternalId', [$document->getInternalId()]), - Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), - Query::limit($database->getLimitForAttributes()), - ]); - - foreach ($attributes as $attribute) { - if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { - $options = $attribute->getAttribute('options'); - foreach ($options as $key => $value) { - $attribute->setAttribute($key, $value); - } - $attribute->removeAttribute('options'); - } - } - - return $attributes; - } -); - -Database::addFilter( - 'subQueryIndexes', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('indexes', [ - Query::equal('collectionInternalId', [$document->getInternalId()]), - Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), - Query::limit($database->getLimitForIndexes()), - ]); - } -); - -Database::addFilter( - 'subQueryPlatforms', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('platforms', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQueryKeys', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('keys', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQueryWebhooks', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('webhooks', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQuerySessions', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database->find('sessions', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryTokens', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database - ->find('tokens', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryChallenges', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database - ->find('challenges', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryAuthenticators', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database - ->find('authenticators', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryMemberships', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database - ->find('memberships', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryVariables', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('variables', [ - Query::equal('resourceInternalId', [$document->getInternalId()]), - Query::equal('resourceType', ['function']), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'encrypt', - function (mixed $value) { - $key = System::getEnv('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - - return json_encode([ - 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag ?? ''), - 'version' => '1', - ]); - }, - function (mixed $value) { - if (is_null($value)) { - return; - } - $value = json_decode($value, true); - $key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']); - - return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); - } -); - -Database::addFilter( - 'subQueryProjectVariables', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('variables', [ - Query::equal('resourceType', ['project']), - Query::limit(APP_LIMIT_SUBQUERY) - ]); - } -); - -Database::addFilter( - 'userSearch', - function (mixed $value, Document $user) { - $searchValues = [ - $user->getId(), - $user->getAttribute('email', ''), - $user->getAttribute('name', ''), - $user->getAttribute('phone', '') - ]; - - foreach ($user->getAttribute('labels', []) as $label) { - $searchValues[] = 'label:' . $label; - } - - $search = implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'subQueryTargets', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database - ->find('targets', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY) - ])); - } -); - -Database::addFilter( - 'subQueryTopicTargets', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - $targetIds = Authorization::skip(fn () => \array_map( - fn ($document) => $document->getAttribute('targetInternalId'), - $database->find('subscribers', [ - Query::equal('topicInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) - ]) - )); - if (\count($targetIds) > 0) { - return $database->skipValidation(fn () => $database->find('targets', [ - Query::equal('$internalId', $targetIds) - ])); - } - return []; - } -); - -Database::addFilter( - 'providerSearch', - function (mixed $value, Document $provider) { - $searchValues = [ - $provider->getId(), - $provider->getAttribute('name', ''), - $provider->getAttribute('provider', ''), - $provider->getAttribute('type', '') - ]; - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'topicSearch', - function (mixed $value, Document $topic) { - $searchValues = [ - $topic->getId(), - $topic->getAttribute('name', ''), - $topic->getAttribute('description', ''), - ]; - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'messageSearch', - function (mixed $value, Document $message) { - $searchValues = [ - $message->getId(), - $message->getAttribute('description', ''), - $message->getAttribute('status', ''), - ]; - - $data = \json_decode($message->getAttribute('data', []), true); - $providerType = $message->getAttribute('providerType', ''); - - if ($providerType === MESSAGE_TYPE_EMAIL) { - $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); - } elseif ($providerType === MESSAGE_TYPE_SMS) { - $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); - } else { - $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); - } - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -/** - * DB Formats - */ -Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () { - return new Email(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () { - return new DatetimeValidator(); -}, Database::VAR_DATETIME); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) { - $elements = $attribute['formatOptions']['elements']; - return new WhiteList($elements, true); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function () { - return new IP(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function () { - return new URL(); -}, Database::VAR_STRING); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { - $min = $attribute['formatOptions']['min'] ?? -INF; - $max = $attribute['formatOptions']['max'] ?? INF; - return new Range($min, $max, Range::TYPE_INTEGER); -}, Database::VAR_INTEGER); - -Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { - $min = $attribute['formatOptions']['min'] ?? -INF; - $max = $attribute['formatOptions']['max'] ?? INF; - return new Range($min, $max, Range::TYPE_FLOAT); -}, Database::VAR_FLOAT); - -/* - * Registry - */ -$register->set('logger', function () { +$registry->set('logger', function () { // Register error logger $providerName = System::getEnv('_APP_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_LOGGING_CONFIG', ''); @@ -776,12 +105,12 @@ $register->set('logger', function () { default => ['key' => $loggingProvider->getHost()], }; } catch (Throwable $th) { - // Fallback for older Appwrite versions up to 1.5.x that use _APP_LOGGING_PROVIDER and _APP_LOGGING_CONFIG environment variables Console::warning('Using deprecated logging configuration. Please update your configuration to use DSN format.' . $th->getMessage()); + // Fallback for older Appwrite versions up to 1.5.x that use _APP_LOGGING_PROVIDER and _APP_LOGGING_CONFIG environment variables $configChunks = \explode(";", $providerConfig); $providerConfig = match ($providerName) { - 'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], + 'sentry' => ['key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], 'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''], default => ['key' => $providerConfig], }; @@ -812,200 +141,167 @@ $register->set('logger', function () { return; } - return new Logger($adapter); + $logger = new Logger($adapter); + $logger->setSample(0.4); + return $logger; }); -$register->set('pools', function () { - $group = new Group(); +$registry->set('geodb', function () { + /** + * @disregard P1009 Undefined type + */ + return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); +}); - $fallbackForDB = 'db_main=' . AppwriteURL::unparse([ - 'scheme' => 'mariadb', - 'host' => System::getEnv('_APP_DB_HOST', 'mariadb'), - 'port' => System::getEnv('_APP_DB_PORT', '3306'), - 'user' => System::getEnv('_APP_DB_USER', ''), - 'pass' => System::getEnv('_APP_DB_PASS', ''), - 'path' => System::getEnv('_APP_DB_SCHEMA', ''), - ]); - $fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([ - 'scheme' => 'redis', - 'host' => System::getEnv('_APP_REDIS_HOST', 'redis'), - 'port' => System::getEnv('_APP_REDIS_PORT', '6379'), - 'user' => System::getEnv('_APP_REDIS_USER', ''), - 'pass' => System::getEnv('_APP_REDIS_PASS', ''), - ]); +$registry->set('hooks', function () { + return new Hooks(); +}); - $connections = [ - 'console' => [ - 'type' => 'database', - 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), - 'multiple' => false, - 'schemes' => ['mariadb', 'mysql'], - ], - 'database' => [ - 'type' => 'database', - 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), - 'multiple' => true, - 'schemes' => ['mariadb', 'mysql'], - ], - 'queue' => [ - 'type' => 'queue', - 'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'pubsub' => [ - 'type' => 'pubsub', - 'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'cache' => [ - 'type' => 'cache', - 'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), - 'multiple' => true, - 'schemes' => ['redis'], - ], - ]; +$registry->set( + 'pools', + (function () { + $fallbackForDB = 'db_main=' . URL::unparse([ + 'scheme' => 'mariadb', + 'host' => System::getEnv('_APP_DB_HOST', 'mariadb'), + 'port' => System::getEnv('_APP_DB_PORT', '3306'), + 'user' => System::getEnv('_APP_DB_USER', ''), + 'pass' => System::getEnv('_APP_DB_PASS', ''), + 'path' => System::getEnv('_APP_DB_SCHEMA', ''), + ]); + $fallbackForRedis = 'redis_main=' . URL::unparse([ + 'scheme' => 'redis', + 'host' => System::getEnv('_APP_REDIS_HOST', 'redis'), + 'port' => System::getEnv('_APP_REDIS_PORT', '6379'), + 'user' => System::getEnv('_APP_REDIS_USER', ''), + 'pass' => System::getEnv('_APP_REDIS_PASS', ''), + ]); - $maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151); - $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); + $connections = [ + 'console' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), + 'multiple' => false, + 'schemes' => ['mariadb', 'mysql'], + ], + 'database' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), + 'multiple' => true, + 'schemes' => ['mariadb', 'mysql'], + ], + 'queue' => [ + 'type' => 'queue', + 'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'pubsub' => [ + 'type' => 'pubsub', + 'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'cache' => [ + 'type' => 'cache', + 'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), + 'multiple' => true, + 'schemes' => ['redis'], + ], + ]; - $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; + $pools = []; + $poolSize = (int)System::getEnv('_APP_POOL_SIZE', 64); - if ($multiprocessing) { - $workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); - } else { - $workerCount = 1; - } + foreach ($connections as $key => $connection) { + $dsns = $connection['dsns'] ?? ''; + $multiple = $connection['multiple'] ?? false; + $schemes = $connection['schemes'] ?? []; + $dsns = explode(',', $connection['dsns'] ?? ''); + $config = []; - if ($workerCount > $instanceConnections) { - throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500); - } + foreach ($dsns as &$dsn) { + $dsn = explode('=', $dsn); + $name = ($multiple) ? $key . '_' . $dsn[0] : $key; + $config[] = $name; + $dsn = $dsn[1] ?? ''; - $poolSize = (int)($instanceConnections / $workerCount); + if (empty($dsn)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); + } - foreach ($connections as $key => $connection) { - $type = $connection['type'] ?? ''; - $multiple = $connection['multiple'] ?? false; - $schemes = $connection['schemes'] ?? []; - $config = []; - $dsns = explode(',', $connection['dsns'] ?? ''); - foreach ($dsns as &$dsn) { - $dsn = explode('=', $dsn); - $name = ($multiple) ? $key . '_' . $dsn[0] : $key; - $dsn = $dsn[1] ?? ''; - $config[] = $name; - if (empty($dsn)) { - //throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); - continue; - } + $dsn = new DSN($dsn); + $dsnHost = $dsn->getHost(); + $dsnPort = $dsn->getPort(); + $dsnUser = $dsn->getUser(); + $dsnPass = $dsn->getPassword(); + $dsnScheme = $dsn->getScheme(); + $dsnDatabase = $dsn->getPath(); - $dsn = new DSN($dsn); - $dsnHost = $dsn->getHost(); - $dsnPort = $dsn->getPort(); - $dsnUser = $dsn->getUser(); - $dsnPass = $dsn->getPassword(); - $dsnScheme = $dsn->getScheme(); - $dsnDatabase = $dsn->getPath(); + if (!in_array($dsnScheme, $schemes)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); + } - if (!in_array($dsnScheme, $schemes)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); - } + /** + * Get Resource + * + * Creation could be reused accross connection types like database, cache, queue, etc. + * + * Resource assignment to an adapter will happen below. + */ + switch ($dsnScheme) { + case 'mysql': + case 'mariadb': + $pool = new PDOPool( + (new PDOConfig()) + ->withHost($dsnHost) + ->withPort($dsnPort) + ->withDbName($dsnDatabase) + ->withCharset('utf8mb4') + ->withUsername($dsnUser) + ->withPassword($dsnPass) + ->withOptions([ + // No need to set PDO::ATTR_ERRMODE it is overwitten in PDOProxy + // PDO::ATTR_TIMEOUT => 3, // Seconds + // PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true, + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, - /** - * Get Resource - * - * Creation could be reused across connection types like database, cache, queue, etc. - * - * Resource assignment to an adapter will happen below. - */ - $resource = match ($dsnScheme) { - 'mysql', - 'mariadb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( - PDO::ATTR_TIMEOUT => 3, // Seconds - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => true, - PDO::ATTR_STRINGIFY_FETCHES => true - )); - }); - }, - 'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) { - $redis = new Redis(); - @$redis->pconnect($dsnHost, (int)$dsnPort); - if ($dsnPass) { - $redis->auth($dsnPass); - } - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; - }, - default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'), - }; - - $pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) { - // Get Adapter - switch ($type) { - case 'database': - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($resource()), - 'mysql' => new MySQL($resource()), - default => null - }; - - $adapter->setDatabase($dsn->getPath()); + ]), + $poolSize + ); break; - case 'pubsub': - $adapter = $resource(); - break; - case 'queue': - $adapter = match ($dsn->getScheme()) { - 'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), - default => null - }; - break; - case 'cache': - $adapter = match ($dsn->getScheme()) { - 'redis' => new RedisCache($resource()), - default => null - }; + case 'redis': + $pool = new RedisPool( + (new RedisConfig()) + ->withHost($dsnHost) + ->withPort((int)$dsnPort) + ->withAuth($dsnPass ?? ''), + $poolSize + ); break; default: - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); } - return $adapter; - }); + $pools['pools-' . $key . '-' . $name] = [ + 'pool' => $pool, + 'dsn' => $dsn, + ]; + } - $group->add($pool); + Config::setParam('pools-' . $key, $config); } - Config::setParam('pools-' . $key, $config); - } + return function () use ($pools): array { + return $pools; + }; + })() +); - return $group; -}); - -$register->set('db', function () { - // This is usually for our workers or CLI commands scope - $dbHost = System::getEnv('_APP_DB_HOST', ''); - $dbPort = System::getEnv('_APP_DB_PORT', ''); - $dbUser = System::getEnv('_APP_DB_USER', ''); - $dbPass = System::getEnv('_APP_DB_PASS', ''); - $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); - - return new PDO( - "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", - $dbUser, - $dbPass, - SQL::getPDOAttributes() - ); -}); - -$register->set('smtp', function () { +$registry->set('smtp', function () { $mail = new PHPMailer(true); $mail->isSMTP(); @@ -1033,739 +329,65 @@ $register->set('smtp', function () { return $mail; }); -$register->set('geodb', function () { - return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); -}); -$register->set('passwordsDictionary', function () { - $content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords'); - $content = explode("\n", $content); - $content = array_flip($content); - return $content; -}); -$register->set('promiseAdapter', function () { + +$registry->set('promiseAdapter', function () { return new Swoole(); }); -$register->set('hooks', function () { - return new Hooks(); -}); -/* - * Localization - */ -Locale::$exceptions = false; -$locales = Config::getParam('locale-codes', []); +$registry->set('db', function () { + // This is usually for our workers or CLI commands scope + $dbHost = System::getEnv('_APP_DB_HOST', ''); + $dbPort = System::getEnv('_APP_DB_PORT', ''); + $dbUser = System::getEnv('_APP_DB_USER', ''); + $dbPass = System::getEnv('_APP_DB_PASS', ''); + $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); -foreach ($locales as $locale) { - $code = $locale['code']; - - $path = __DIR__ . '/config/locale/translations/' . $code . '.json'; - - if (!\file_exists($path)) { - $path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar` - if (!\file_exists($path)) { - $path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json` - } - } - - Locale::setLanguageFromJSON($code, $path); -} - -\stream_context_set_default([ // Set global user agent and http settings - 'http' => [ - 'method' => 'GET', - 'user_agent' => \sprintf( - APP_USERAGENT, - System::getEnv('_APP_VERSION', 'UNKNOWN'), - System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) - ), - 'timeout' => 2, - ], -]); - -// Runtime Execution -App::setResource('log', fn () => new Log()); -App::setResource('logger', function ($register) { - return $register->get('logger'); -}, ['register']); - -App::setResource('hooks', function ($register) { - return $register->get('hooks'); -}, ['register']); - -App::setResource('register', fn () => $register); -App::setResource('locale', fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); - -App::setResource('localeCodes', function () { - return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])); -}); - -// Queues -App::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); -App::setResource('queueForMessaging', function (Connection $queue) { - return new Messaging($queue); -}, ['queue']); -App::setResource('queueForMails', function (Connection $queue) { - return new Mail($queue); -}, ['queue']); -App::setResource('queueForBuilds', function (Connection $queue) { - return new Build($queue); -}, ['queue']); -App::setResource('queueForDatabase', function (Connection $queue) { - return new EventDatabase($queue); -}, ['queue']); -App::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); -App::setResource('queueForEvents', function (Connection $queue) { - return new Event($queue); -}, ['queue']); -App::setResource('queueForAudits', function (Connection $queue) { - return new Audit($queue); -}, ['queue']); -App::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); -App::setResource('queueForUsage', function (Connection $queue) { - return new Usage($queue); -}, ['queue']); -App::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); -App::setResource('queueForMigrations', function (Connection $queue) { - return new Migration($queue); -}, ['queue']); -App::setResource('clients', function ($request, $console, $project) { - $console->setAttribute('platforms', [ // Always allow current host - '$collection' => ID::custom('platforms'), - 'name' => 'Current Host', - 'type' => Origin::CLIENT_TYPE_WEB, - 'hostname' => $request->getHostname(), - ], Document::SET_TYPE_APPEND); - - $hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', '')); - $validator = new Hostname(); - foreach ($hostnames as $hostname) { - $hostname = trim($hostname); - if (!$validator->isValid($hostname)) { - continue; - } - $console->setAttribute('platforms', [ - '$collection' => ID::custom('platforms'), - 'type' => Origin::CLIENT_TYPE_WEB, - 'name' => $hostname, - 'hostname' => $hostname, - ], Document::SET_TYPE_APPEND); - } - - /** - * Get All verified client URLs for both console and current projects - * + Filter for duplicated entries - */ - $clientsConsole = \array_map( - fn ($node) => $node['hostname'], - \array_filter( - $console->getAttribute('platforms', []), - fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && !empty($node['hostname'])) - ) + return new PDO( + "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", + $dbUser, + $dbPass, + SQL::getPDOAttributes() ); - - $clients = $clientsConsole; - $platforms = $project->getAttribute('platforms', []); - - foreach ($platforms as $node) { - if ( - isset($node['type']) && - ($node['type'] === Origin::CLIENT_TYPE_WEB || - $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && - !empty($node['hostname']) - ) { - $clients[] = $node['hostname']; - } - } - - return \array_unique($clients); -}, ['request', 'console', 'project']); - -App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Database\Document $project */ - /** @var Utopia\Database\Database $dbForProject */ - /** @var Utopia\Database\Database $dbForConsole */ - /** @var string $mode */ - - Authorization::setDefaultStatus(true); - - Auth::setCookieName('a_session_' . $project->getId()); - - if (APP_MODE_ADMIN === $mode) { - Auth::setCookieName('a_session_' . $console->getId()); - } - - $session = Auth::decodeSession( - $request->getCookie( - Auth::$cookieName, // Get sessions - $request->getCookie(Auth::$cookieName . '_legacy', '') - ) - ); - - // Get session from header for SSR clients - if (empty($session['id']) && empty($session['secret'])) { - $sessionHeader = $request->getHeader('x-appwrite-session', ''); - - if (!empty($sessionHeader)) { - $session = Auth::decodeSession($sessionHeader); - } - } - - // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { - $response->addHeader('X-Debug-Fallback', 'false'); - } - - if (empty($session['id']) && empty($session['secret'])) { - if ($response) { - $response->addHeader('X-Debug-Fallback', 'true'); - } - $fallback = $request->getHeader('x-fallback-cookies', ''); - $fallback = \json_decode($fallback, true); - $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); - } - - Auth::$unique = $session['id'] ?? ''; - Auth::$secret = $session['secret'] ?? ''; - - if (APP_MODE_ADMIN !== $mode) { - if ($project->isEmpty()) { - $user = new Document([]); - } else { - if ($project->getId() === 'console') { - $user = $dbForConsole->getDocument('users', Auth::$unique); - } else { - $user = $dbForProject->getDocument('users', Auth::$unique); - } - } - } else { - $user = $dbForConsole->getDocument('users', Auth::$unique); - } - - if ( - $user->isEmpty() // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) - ) { // Validate user has valid login token - $user = new Document([]); - } - - if (APP_MODE_ADMIN === $mode) { - if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { - Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. - } else { - $user = new Document([]); - } - } - - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - - if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); - - try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); - } - - $jwtUserId = $payload['userId'] ?? ''; - if (!empty($jwtUserId)) { - $user = $dbForProject->getDocument('users', $jwtUserId); - } - - $jwtSessionId = $payload['sessionId'] ?? ''; - if (!empty($jwtSessionId)) { - if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new Document([]); - } - } - } - - $dbForProject->setMetadata('user', $user->getId()); - $dbForConsole->setMetadata('user', $user->getId()); - - return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']); - -App::setResource('project', function ($dbForConsole, $request, $console) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Utopia\Database\Database $dbForConsole */ - /** @var Utopia\Database\Document $console */ - - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - - if (empty($projectId) || $projectId === 'console') { - return $console; - } - - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); - - return $project; -}, ['dbForConsole', 'request', 'console']); - -App::setResource('session', function (Document $user) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); - - if (!$sessionId) { - return; - } - - foreach ($sessions as $session) {/** @var Document $session */ - if ($sessionId === $session->getId()) { - return $session; - } - } - - return; -}, ['user']); - -App::setResource('console', function () { - return new Document([ - '$id' => ID::custom('console'), - '$internalId' => ID::custom('console'), - 'name' => 'Appwrite', - '$collection' => ID::custom('projects'), - 'description' => 'Appwrite core engine', - 'logo' => '', - 'teamId' => -1, - 'webhooks' => [], - 'keys' => [], - 'platforms' => [ - [ - '$collection' => ID::custom('platforms'), - 'name' => 'Localhost', - 'type' => Origin::CLIENT_TYPE_WEB, - 'hostname' => 'localhost', - ], // Current host is added on app init - ], - 'legalName' => '', - 'legalCountry' => '', - 'legalState' => '', - 'legalCity' => '', - 'legalAddress' => '', - 'legalTaxId' => '', - 'auths' => [ - 'mockNumbers' => [], - 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', - 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds - 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' - ], - 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], - 'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'oAuthProviders' => [ - 'githubEnabled' => true, - 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), - 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') - ], - ]); -}, []); - -App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; -}, ['pools', 'dbForConsole', 'cache', 'project']); - -App::setResource('dbForConsole', function (Group $pools, Cache $cache) { - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - return $database; -}, ['pools', 'cache']); - -App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $configure = (function (Database $database) use ($project, $dsn) { - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()) - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - }); - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - $configure($database); - return $database; - } - - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); - $databases[$dsn->getHost()] = $database; - $configure($database); - - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -App::setResource('cache', function (Group $pools) { - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); -}, ['pools']); - -App::setResource('deviceForLocal', function () { - return new Local(); }); -App::setResource('deviceForFiles', function ($project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); -}, ['project']); +// Autoload +class_exists(JWT::class, true); +class_exists(DSN::class, true); +class_exists(Log::class, true); +class_exists(TOTP::class, true); +class_exists(Mail::class, true); +class_exists(Func::class, true); +class_exists(Cache::class, true); +class_exists(Abuse::class, true); +class_exists(MySQL::class, true); +class_exists(Event::class, true); +class_exists(Audit::class, true); +class_exists(Usage::class, true); +class_exists(Local::class, true); +class_exists(Build::class, true); +class_exists(Locale::class, true); +class_exists(Delete::class, true); +class_exists(GitHub::class, true); +class_exists(Schema::class, true); +class_exists(Domain::class, true); +class_exists(Console::class, true); +class_exists(Request::class, true); +class_exists(MariaDB::class, true); +class_exists(Document::class, true); +class_exists(Sharding::class, true); +class_exists(Database::class, true); +class_exists(Hostname::class, true); +class_exists(TimeLimit::class, true); +class_exists(Migration::class, true); +class_exists(Messaging::class, true); +class_exists(CacheRedis::class, true); +class_exists(Connections::class, true); +class_exists(Certificate::class, true); +class_exists(EventDatabase::class, true); +class_exists(Authorization::class, true); +class_exists(Authentication::class, true); +class_exists(Queue\Connection\Redis::class, true); -App::setResource('deviceForFunctions', function ($project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); -}, ['project']); +require_once __DIR__ . '/init/resources.php'; -App::setResource('deviceForBuilds', function ($project) { - return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); -}, ['project']); - -function getDevice($root): Device -{ - $connection = System::getEnv('_APP_CONNECTIONS_STORAGE', ''); - - if (!empty($connection)) { - $acl = 'private'; - $device = Storage::DEVICE_LOCAL; - $accessKey = ''; - $accessSecret = ''; - $bucket = ''; - $region = ''; - - try { - $dsn = new DSN($connection); - $device = $dsn->getScheme(); - $accessKey = $dsn->getUser() ?? ''; - $accessSecret = $dsn->getPassword() ?? ''; - $bucket = $dsn->getPath() ?? ''; - $region = $dsn->getParam('region'); - } catch (\Throwable $e) { - Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); - } - - switch ($device) { - case Storage::DEVICE_S3: - return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case STORAGE::DEVICE_DO_SPACES: - $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); - $device->setHttpVersion(S3::HTTP_VERSION_1_1); - return $device; - case Storage::DEVICE_BACKBLAZE: - return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LINODE: - return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_WASABI: - return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - } - } else { - switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - case Storage::DEVICE_S3: - $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); - $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); - $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); - $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); - $s3Acl = 'private'; - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); - case Storage::DEVICE_DO_SPACES: - $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); - $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); - $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); - $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); - $doSpacesAcl = 'private'; - $device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); - $device->setHttpVersion(S3::HTTP_VERSION_1_1); - return $device; - case Storage::DEVICE_BACKBLAZE: - $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); - $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); - $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); - $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); - $backblazeAcl = 'private'; - return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); - case Storage::DEVICE_LINODE: - $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); - $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); - $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); - $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); - $linodeAcl = 'private'; - return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); - case Storage::DEVICE_WASABI: - $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); - $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); - $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); - $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); - $wasabiAcl = 'private'; - return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); - } - } -} - -App::setResource('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); - -App::setResource('geodb', function ($register) { - /** @var Utopia\Registry\Registry $register */ - return $register->get('geodb'); -}, ['register']); - -App::setResource('passwordsDictionary', function ($register) { - /** @var Utopia\Registry\Registry $register */ - return $register->get('passwordsDictionary'); -}, ['register']); - - -App::setResource('servers', function () { - $platforms = Config::getParam('platforms'); - $server = $platforms[APP_PLATFORM_SERVER]; - - $languages = array_map(function ($language) { - return strtolower($language['name']); - }, $server['sdks']); - - return $languages; -}); - -App::setResource('promiseAdapter', function ($register) { - return $register->get('promiseAdapter'); -}, ['register']); - -App::setResource('schema', function ($utopia, $dbForProject) { - - $complexity = function (int $complexity, array $args) { - $queries = Query::parseQueries($args['queries'] ?? []); - $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; - $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; - - return $complexity * $limit; - }; - - $attributes = function (int $limit, int $offset) use ($dbForProject) { - $attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [ - Query::limit($limit), - Query::offset($offset), - ])); - - return \array_map(function ($attr) { - return $attr->getArrayCopy(); - }, $attrs); - }; - - $urls = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'read' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'delete' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - ]; - - $params = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return [ 'queries' => $args['queries']]; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - $id = $args['id'] ?? 'unique()'; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'documentId' => $id, - 'collectionId' => $collectionId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - $documentId = $args['id']; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - // Order must be the same as the route params - return [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => $documentId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - ]; - - return Schema::build( - $utopia, - $complexity, - $attributes, - $urls, - $params, - ); -}, ['utopia', 'dbForProject']); - -App::setResource('contributors', function () { - $path = 'app/config/contributors.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('employees', function () { - $path = 'app/config/employees.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('heroes', function () { - $path = 'app/config/heroes.json'; - $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; - return $list; -}); - -App::setResource('gitHub', function (Cache $cache) { - return new VcsGitHub($cache); -}, ['cache']); - -App::setResource('requestTimestamp', function ($request) { - //TODO: Move this to the Request class itself - $timestampHeader = $request->getHeader('x-appwrite-timestamp'); - $requestTimestamp = null; - if (!empty($timestampHeader)) { - try { - $requestTimestamp = new \DateTime($timestampHeader); - } catch (\Throwable $e) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); - } - } - return $requestTimestamp; -}, ['request']); -App::setResource('plan', function (array $plan = []) { - return []; -}); +Models::init(); diff --git a/app/init/config.php b/app/init/config.php new file mode 100644 index 0000000000..bc8333b5d9 --- /dev/null +++ b/app/init/config.php @@ -0,0 +1,37 @@ + $value], JSON_PRESERVE_ZERO_FRACTION); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + + return json_decode($value, true)['value']; + } +); + +Database::addFilter( + 'enum', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('elements')) { + $attribute->removeAttribute('elements'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['elements'])) { + $attribute->setAttribute('elements', $formatOptions['elements']); + } + + return $value; + } +); + +Database::addFilter( + 'range', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('min')) { + $attribute->removeAttribute('min'); + } + if ($attribute->isSet('max')) { + $attribute->removeAttribute('max'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['min']) || isset($formatOptions['max'])) { + $attribute + ->setAttribute('min', $formatOptions['min']) + ->setAttribute('max', $formatOptions['max']) + ; + } + + return $value; + } +); + +Database::addFilter( + 'subQueryAttributes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $attributes = $database->find('attributes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForAttributes()), + ]); + + foreach ($attributes as $attribute) { + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options'); + foreach ($options as $key => $value) { + $attribute->setAttribute($key, $value); + } + $attribute->removeAttribute('options'); + } + } + + return $attributes; + } +); + +Database::addFilter( + 'subQueryIndexes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('indexes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForIndexes()), + ]); + } +); + +Database::addFilter( + 'subQueryPlatforms', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('platforms', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryKeys', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('keys', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryWebhooks', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('webhooks', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQuerySessions', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database->find('sessions', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryTokens', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('tokens', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryChallenges', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('challenges', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryAuthenticators', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('authenticators', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryMemberships', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('memberships', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceInternalId', [$document->getInternalId()]), + Query::equal('resourceType', ['function']), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'encrypt', + function (mixed $value) { + $key = System::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + + return json_encode([ + 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag ?? ''), + 'version' => '1', + ]); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + $value = json_decode($value, true); + $key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']); + + return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); + } +); + +Database::addFilter( + 'subQueryProjectVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceType', ['project']), + Query::limit(APP_LIMIT_SUBQUERY) + ]); + } +); + +Database::addFilter( + 'userSearch', + function (mixed $value, Document $user) { + $searchValues = [ + $user->getId(), + $user->getAttribute('email', ''), + $user->getAttribute('name', ''), + $user->getAttribute('phone', '') + ]; + + foreach ($user->getAttribute('labels', []) as $label) { + $searchValues[] = 'label:' . $label; + } + + $search = implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'subQueryTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('targets', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY) + ])); + } +); + +Database::addFilter( + 'subQueryTopicTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $targetIds = $database->getAuthorization()->skip(fn () => \array_map( + fn ($document) => $document->getAttribute('targetInternalId'), + $database->find('subscribers', [ + Query::equal('topicInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) + ]) + )); + if (\count($targetIds) > 0) { + return $database->find('targets', [ + Query::equal('$internalId', $targetIds) + ]); + } + return []; + } +); + +Database::addFilter( + 'providerSearch', + function (mixed $value, Document $provider) { + $searchValues = [ + $provider->getId(), + $provider->getAttribute('name', ''), + $provider->getAttribute('provider', ''), + $provider->getAttribute('type', '') + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'topicSearch', + function (mixed $value, Document $topic) { + $searchValues = [ + $topic->getId(), + $topic->getAttribute('name', ''), + $topic->getAttribute('description', ''), + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'messageSearch', + function (mixed $value, Document $message) { + $searchValues = [ + $message->getId(), + $message->getAttribute('description', ''), + $message->getAttribute('status', ''), + ]; + + $data = \json_decode($message->getAttribute('data', []), true); + $providerType = $message->getAttribute('providerType', ''); + + if ($providerType === MESSAGE_TYPE_EMAIL) { + $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); + } elseif ($providerType === MESSAGE_TYPE_SMS) { + $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); + } else { + $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); + } + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); diff --git a/app/init/database/formats.php b/app/init/database/formats.php new file mode 100644 index 0000000000..88e46655ac --- /dev/null +++ b/app/init/database/formats.php @@ -0,0 +1,43 @@ +getScheme(); + $accessKey = $dsn->getUser() ?? ''; + $accessSecret = $dsn->getPassword() ?? ''; + $bucket = $dsn->getPath() ?? ''; + $region = $dsn->getParam('region'); + } catch (\Throwable $e) { + Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); + } + + switch ($device) { + case Storage::DEVICE_S3: + return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case STORAGE::DEVICE_DO_SPACES: + return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_BACKBLAZE: + return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LINODE: + return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_WASABI: + return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + } + } else { + switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + case Storage::DEVICE_S3: + $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); + $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); + $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); + $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); + $s3Acl = 'private'; + return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); + case Storage::DEVICE_DO_SPACES: + $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); + $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); + $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); + $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); + $doSpacesAcl = 'private'; + return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); + case Storage::DEVICE_BACKBLAZE: + $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); + $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); + $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); + $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); + $backblazeAcl = 'private'; + return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); + case Storage::DEVICE_LINODE: + $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); + $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); + $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); + $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); + $linodeAcl = 'private'; + return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); + case Storage::DEVICE_WASABI: + $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); + $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); + $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); + $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); + $wasabiAcl = 'private'; + return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); + } + } +} + +$log = new Dependency(); +$mode = new Dependency(); +$user = new Dependency(); +$plan = new Dependency(); +$pools = new Dependency(); +$geodb = new Dependency(); +$cache = new Dependency(); +$pools = new Dependency(); +$queue = new Dependency(); +$hooks = new Dependency(); +$logger = new Dependency(); +$locale = new Dependency(); +$schema = new Dependency(); +$github = new Dependency(); +$session = new Dependency(); +$console = new Dependency(); +$project = new Dependency(); +$clients = new Dependency(); +$servers = new Dependency(); +$register = new Dependency(); +$connections = new Dependency(); +$localeCodes = new Dependency(); +$getConsoleDB = new Dependency(); +$getProjectDB = new Dependency(); +$dbForProject = new Dependency(); +$dbForConsole = new Dependency(); +$queueForUsage = new Dependency(); +$queueForMails = new Dependency(); +$authorization = new Dependency(); +$authentication = new Dependency(); +$queueForBuilds = new Dependency(); +$deviceForLocal = new Dependency(); +$deviceForFiles = new Dependency(); +$queueForEvents = new Dependency(); +$queueForAudits = new Dependency(); +$promiseAdapter = new Dependency(); +$schemaVariable = new Dependency(); +$deviceForBuilds = new Dependency(); +$queueForDeletes = new Dependency(); +$requestTimestamp = new Dependency(); +$queueForDatabase = new Dependency(); +$queueForMessaging = new Dependency(); +$queueForFunctions = new Dependency(); +$queueForMigrations = new Dependency(); +$deviceForFunctions = new Dependency(); +$passwordsDictionary = new Dependency(); +$queueForCertificates = new Dependency(); + + +$plan + ->setName('plan') + ->setCallback(fn () => []); + +$mode + ->setName('mode') + ->inject('request') + ->setCallback(function (Request $request) { + /** + * Defines the mode for the request: + * - 'default' => Requests for Client and Server Side + * - 'admin' => Request from the Console on non-console projects + */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + }); + +$user + ->setName('user') + ->inject('mode') + ->inject('project') + ->inject('console') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForConsole') + ->inject('authorization') + ->inject('authentication') + ->setCallback(function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization, Authentication $authentication) { + $authorization->setDefaultStatus(true); + $authentication->setCookieName('a_session_' . $project->getId()); + + if (APP_MODE_ADMIN === $mode) { + $authentication->setCookieName('a_session_' . $console->getId()); + } + + $session = Auth::decodeSession( + $request->getCookie( + $authentication->getCookieName(), // Get sessions + $request->getCookie($authentication->getCookieName() . '_legacy', '') + ) + ); + + // Get session from header for SSR clients + if (empty($session['id']) && empty($session['secret'])) { + $sessionHeader = $request->getHeader('x-appwrite-session', ''); + + if (!empty($sessionHeader)) { + $session = Auth::decodeSession($sessionHeader); + } + } + + // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies + if ($response) { + $response->addHeader('X-Debug-Fallback', 'false'); + } + + if (empty($session['id']) && empty($session['secret'])) { + if ($response) { + $response->addHeader('X-Debug-Fallback', 'true'); + } + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $session = Auth::decodeSession(((isset($fallback[$authentication->getCookieName()])) ? $fallback[$authentication->getCookieName()] : '')); + } + + $authentication->setUnique($session['id'] ?? ''); + $authentication->setSecret($session['secret'] ?? ''); + + if (APP_MODE_ADMIN !== $mode) { + if ($project->isEmpty()) { + $user = new Document([]); + } else { + if ($project->getId() === 'console') { + $user = $dbForConsole->getDocument('users', $authentication->getUnique()); + } else { + $user = $dbForProject->getDocument('users', $authentication->getUnique()); + } + } + } else { + $user = $dbForConsole->getDocument('users', $authentication->getUnique()); + } + + if ( + $user->isEmpty() // Check a document has been found in the DB + || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) + ) { // Validate user has valid login token + $user = new Document([]); + } + + if (APP_MODE_ADMIN === $mode) { + if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) { + $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. + } else { + $user = new Document([]); + } + } + + $authJWT = $request->getHeader('x-appwrite-jwt', ''); + + if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + $request->removeHeader('x-appwrite-jwt'); + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + if (!empty($jwtUserId)) { + $user = $dbForProject->getDocument('users', $jwtUserId); + } + + $jwtSessionId = $payload['sessionId'] ?? ''; + if (!empty($jwtSessionId)) { + if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token + $user = new Document([]); + } + } + } + + // Adds logs to database queries + $dbForProject->setMetadata('user', $user->getId()); + $dbForConsole->setMetadata('user', $user->getId()); + + return $user; + }); + + +$session + ->setName('session') + ->inject('user') + ->inject('project') + ->inject('authorization') + ->inject('authentication') + ->setCallback(function (Document $user, Document $project, Authorization $authorization, Authentication $authentication) { + if ($user->isEmpty()) { + return; + } + + $sessions = $user->getAttribute('sessions', []); + $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret(), $authDuration); + + if (!$sessionId) { + return; + } + + foreach ($sessions as $session) { + if ($sessionId === $session->getId()) { + return $session; + } + } + + return; + }); + +$console + ->setName('console') + ->setCallback(function () { + return new Document([ + '$id' => ID::custom('console'), + '$internalId' => ID::custom('console'), + 'name' => 'Appwrite', + '$collection' => ID::custom('projects'), + 'description' => 'Appwrite core engine', + 'logo' => '', + 'teamId' => -1, + 'webhooks' => [], + 'keys' => [], + 'platforms' => [ + [ + '$collection' => ID::custom('platforms'), + 'name' => 'Localhost', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => 'localhost', + ], // Current host is added on app init + ], + 'legalName' => '', + 'legalCountry' => '', + 'legalState' => '', + 'legalCity' => '', + 'legalAddress' => '', + 'legalTaxId' => '', + 'auths' => [ + 'mockNumbers' => [], + 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', + 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user + 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds + 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' + ], + 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], + 'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'oAuthProviders' => [ + 'githubEnabled' => true, + 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), + 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') + ], + ]); + }); + + +$project + ->setName('project') + ->inject('dbForConsole') + ->inject('request') + ->inject('console') + ->inject('authorization') + ->setCallback(function (Database $dbForConsole, Request $request, Document $console, Authorization $authorization) { + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + + if (empty($projectId) || $projectId === 'console') { + return $console; + } + + $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + + return $project; + }); + +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); + +$dbForProject + ->setName('dbForProject') + ->inject('cache') + ->inject('pools') + ->inject('project') + ->inject('dbForConsole') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (Cache $cache, array $pools, Document $project, Database $dbForConsole, Authorization $authorization, Connections $connections) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($connectionDsn->getPath()); + + $database = new Database($adapter, $cache); + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + $database->setAuthorization($authorization); + return $database; + }); + +$dbForConsole + ->setName('dbForConsole') + ->inject('getConsoleDB') + ->inject('connections') + ->setCallback(function (callable $getConsoleDB, Connections $connections): Database { + [$connection,$pool, $database] = $getConsoleDB(); + $connections->add($connection, $pool); + + return $database; + }); + +$cache + ->setName('cache') + ->inject('pools') + ->inject('connections') + ->setCallback(function (array $pools, Connections $connections) { + $adapters = []; + $databases = Config::getParam('pools-cache'); + + foreach ($databases as $database) { + $pool = $pools['pools-cache-' . $database]['pool']; + $dsn = $pools['pools-cache-' . $database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapters[] = new CacheRedis($connection); + } + + return new Cache(new Sharding($adapters)); + }); + +$authorization + ->setName('authorization') + ->setCallback(function (): Authorization { + return new Authorization(); + }); + +$authentication + ->setName('authentication') + ->setCallback(function (): Authentication { + return new Authentication(); + }); + +$register + ->setName('registry') + ->setCallback(function () use (&$registry): Registry { + return $registry; + }); + +$pools + ->setName('pools') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('pools'); + }); + +$logger + ->setName('logger') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('logger'); + }); + +$log + ->setName('log') + ->setCallback(function () { + return new Log(); + }); + +$connections + ->setName('connections') + ->setCallback(function () { + return new Connections(); + }); + +$locale + ->setName('locale') + ->setCallback(fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); + +$localeCodes + ->setName('localeCodes') + ->setCallback(fn () => array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []))); + +$queue + ->setName('queue') + ->inject('pools') + ->inject('connections') + ->setCallback(function (array $pools, Connections $connections) { + $pool = $pools['pools-queue-queue']['pool']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + return new Queue\Connection\Redis($connection); + }); + +$queueForMessaging + ->setName('queueForMessaging') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Messaging($queue); + }); + +$queueForMails + ->setName('queueForMails') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Mail($queue); + }); + +$queueForBuilds + ->setName('queueForBuilds') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Build($queue); + }); + +$queueForDatabase + ->setName('queueForDatabase') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new EventDatabase($queue); + }); + +$queueForDeletes + ->setName('queueForDeletes') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Delete($queue); + }); + +$queueForEvents + ->setName('queueForEvents') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Event($queue); + }); + +$queueForAudits + ->setName('queueForAudits') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Audit($queue); + }); + +$queueForFunctions + ->setName('queueForFunctions') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Func($queue); + }); + +$queueForUsage + ->setName('queueForUsage') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Usage($queue); + }); + +$queueForCertificates + ->setName('queueForCertificates') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Certificate($queue); + }); + +$queueForMigrations + ->setName('queueForMigrations') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new Migration($queue); + }); + +$deviceForLocal + ->setName('deviceForLocal') + ->setCallback(function () { + return new Local(); + }); + +$deviceForFiles + ->setName('deviceForFiles') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); + }); + +$deviceForFunctions + ->setName('deviceForFunctions') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); + }); + +$deviceForBuilds + ->setName('deviceForBuilds') + ->inject('project') + ->setCallback(function ($project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); + }); + +$clients + ->setName('clients') + ->inject('request') + ->inject('console') + ->inject('project') + ->setCallback(function (Request $request, Document $console, Document $project) { + $console->setAttribute('platforms', [ // Always allow current host + '$collection' => ID::custom('platforms'), + 'name' => 'Current Host', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => $request->getHostname(), + ], Document::SET_TYPE_APPEND); + + $hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', '')); + $validator = new Hostname(); + foreach ($hostnames as $hostname) { + $hostname = trim($hostname); + if (!$validator->isValid($hostname)) { + continue; + } + $console->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Origin::CLIENT_TYPE_WEB, + 'name' => $hostname, + 'hostname' => $hostname, + ], Document::SET_TYPE_APPEND); + } + + /** + * Get All verified client URLs for both console and current projects + * + Filter for duplicated entries + */ + $clientsConsole = \array_map( + fn ($node) => $node['hostname'], + \array_filter( + $console->getAttribute('platforms', []), + fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && isset($node['hostname']) && !empty($node['hostname'])) + ) + ); + + $clients = \array_unique( + \array_merge( + $clientsConsole, + \array_map( + fn ($node) => $node['hostname'], + \array_filter( + $project->getAttribute('platforms', []), + fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB || $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && isset($node['hostname']) && !empty($node['hostname'])) + ) + ) + ) + ); + + return $clients; + }); + +$servers + ->setName('servers') + ->setCallback(function () { + $platforms = Config::getParam('platforms'); + $server = $platforms[APP_PLATFORM_SERVER]; + + $languages = array_map(function ($language) { + return strtolower($language['name']); + }, $server['sdks']); + + return $languages; + }); + +$geodb + ->setName('geodb') + ->inject('registry') + ->setCallback(function (Registry $register) { + return $register->get('geodb'); + }); + +$passwordsDictionary + ->setName('passwordsDictionary') + ->setCallback(function () { + $content = file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords'); + $content = explode("\n", $content); + $content = array_flip($content); + return $content; + }); + +$hooks + ->setName('hooks') + ->inject('registry') + ->setCallback(function (Registry $registry) { + return $registry->get('hooks'); + }); + +$github + ->setName('gitHub') + ->inject('cache') + ->setCallback(function (Cache $cache) { + return new GitHub($cache); + }); + +$requestTimestamp + ->setName('requestTimestamp') + ->inject('request') + ->setCallback(function ($request) { + $timestampHeader = $request->getHeader('x-appwrite-timestamp'); + $requestTimestamp = null; + if (!empty($timestampHeader)) { + try { + $requestTimestamp = new \DateTime($timestampHeader); + } catch (\Throwable $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); + } + } + return $requestTimestamp; + }); +$getConsoleDB + ->setName('getConsoleDB') + ->inject('pools') + ->inject('cache') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (array $pools, Cache $cache, Authorization $authorization) { + return function () use ($pools, $cache, $authorization): array { + $pool = $pools['pools-console-console']['pool']; + $dsn = $pools['pools-console-console']['dsn']; + $connection = $pool->get(); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + return [$connection, $pool, $database]; + }; + }); + +$getProjectDB + ->setName('getProjectDB') + ->inject('pools') + ->inject('dbForConsole') + ->inject('cache') + ->inject('authorization') + ->inject('connections') + ->setCallback(function (array $pools, Database $dbForConsole, Cache $cache, Authorization $authorization, Connections $connections) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases, $authorization, $connections): Database { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if (isset($databases[$dsn->getHost()])) { + $database = $databases[$dsn->getHost()]; + + if ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; + } + + $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; + $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($connectionDsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($connectionDsn->getPath()); + + $database = new Database($adapter, $cache); + $database->setAuthorization($authorization); + + $databases[$dsn->getHost()] = $database; + + if ($dsn->getHost() === DATABASE_SHARED_TABLES) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; + }; + }); + +$promiseAdapter + ->setName('promiseAdapter') + ->setCallback(function () use ($registry) { + return $registry->get('promiseAdapter'); + }); + +$schemaVariable + ->setName('schemaVariable') + ->setCallback(fn () => new Schema()); + +$schema + ->setName('schema') + ->inject('http') + ->inject('context') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('authorization') + ->inject('schemaVariable') + ->setCallback(function (Http $http, Container $context, Request $request, Response $response, Database $dbForProject, Authorization $authorization, $schemaVariable) { + $complexity = function (int $complexity, array $args) { + $queries = Query::parseQueries($args['queries'] ?? []); + $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; + $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; + + return $complexity * $limit; + }; + + $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { + $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ + Query::limit($limit), + Query::offset($offset), + ])); + + return \array_map(function ($attr) { + return $attr->getArrayCopy(); + }, $attrs); + }; + + $urls = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'read' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'delete' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + ]; + + $params = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return ['queries' => $args['queries']]; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + $id = $args['id'] ?? 'unique()'; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + return [ + 'databaseId' => $databaseId, + 'documentId' => $id, + 'collectionId' => $collectionId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + $documentId = $args['id']; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => $documentId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + ]; + + return $schemaVariable->build( + $http, + $request, + $response, + $context, + $complexity, + $attributes, + $urls, + $params, + ); + }); + +$container->set($log); +$container->set($mode); +$container->set($user); +$container->set($plan); +$container->set($cache); +$container->set($pools); +$container->set($queue); +$container->set($geodb); +$container->set($hooks); +$container->set($locale); +$container->set($schema); +$container->set($github); +$container->set($logger); +$container->set($session); +$container->set($console); +$container->set($project); +$container->set($clients); +$container->set($servers); +$container->set($register); +$container->set($connections); +$container->set($localeCodes); +$container->set($dbForProject); +$container->set($dbForConsole); +$container->set($getConsoleDB); +$container->set($getProjectDB); +$container->set($queueForUsage); +$container->set($queueForMails); +$container->set($authorization); +$container->set($authentication); +$container->set($schemaVariable); +$container->set($queueForBuilds); +$container->set($queueForEvents); +$container->set($queueForAudits); +$container->set($deviceForLocal); +$container->set($deviceForFiles); +$container->set($promiseAdapter); +$container->set($queueForDeletes); +$container->set($deviceForBuilds); +$container->set($queueForDatabase); +$container->set($requestTimestamp); +$container->set($queueForMessaging); +$container->set($queueForFunctions); +$container->set($queueForMigrations); +$container->set($deviceForFunctions); +$container->set($passwordsDictionary); +$container->set($queueForCertificates); diff --git a/app/realtime.php b/app/realtime.php index 39e00f7dda..ee6dd4fa81 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,136 +5,47 @@ use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; +use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Http\Request as SwooleRequest; +use Swoole\Http\Response as SwooleHttpResponse; use Swoole\Http\Response as SwooleResponse; use Swoole\Runtime; use Swoole\Table; use Swoole\Timer; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; -use Utopia\App; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Config\Config; -use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Http\Adapter\Swoole\Request as UtopiaRequest; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; +use Utopia\Http\Adapter\Swoole\Response as UtopiaResponse; +use Utopia\Http\Http; use Utopia\Logger\Log; +use Utopia\Pools\Connection; +use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\WebSocket\Adapter; use Utopia\WebSocket\Server; /** - * @var \Utopia\Registry\Registry $register + * @var Registry $registry + * @var Container $container */ +global $registry, $container; + + require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -// Allows overriding -if (!function_exists("getConsoleDB")) { - function getConsoleDB(): Database - { - global $register; - - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); - - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource() - ; - - $database = new Database($dbAdapter, getCache()); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', '_console'); - - return $database; - } -} - -// Allows overriding -if (!function_exists("getProjectDB")) { - function getProjectDB(Document $project): Database - { - global $register; - - /** @var \Utopia\Pools\Group $pools */ - $pools = $register->get('pools'); - - if ($project->isEmpty() || $project->getId() === 'console') { - return getConsoleDB(); - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $adapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($adapter, getCache()); - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - $database - ->setMetadata('host', \gethostname()) - ->setMetadata('project', $project->getId()); - - return $database; - } -} - -// Allows overriding -if (!function_exists("getCache")) { - function getCache(): Cache - { - global $register; - - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ - - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); - } -} - $realtime = new Realtime(); /** @@ -159,8 +70,8 @@ $adapter $server = new Server($adapter); -$logError = function (Throwable $error, string $action) use ($register) { - $logger = $register->get('logger'); +$logError = function (Throwable $error, string $action) use ($registry) { + $logger = $registry->get('logger'); if ($logger && !$error instanceof Exception) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); @@ -200,16 +111,16 @@ $logError = function (Throwable $error, string $action) use ($register) { $server->error($logError); -$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) { +$server->onStart(function () use ($stats, $container, $containerId, &$statsDocument, $logError) { sleep(5); // wait for the initial database schema to be ready Console::success('Server started successfully'); - + $authorization = $container->get('authorization'); /** * Create document for this worker to share stats across Containers. */ - go(function () use ($register, $containerId, &$statsDocument) { + go(function () use ($container, $containerId, &$statsDocument) { $attempts = 0; - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); do { try { @@ -223,14 +134,15 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume 'value' => '{}' ]); - $statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); + $authorization = $container->get('authorization'); + $statsDocument = $authorization->skip(fn () => $database->createDocument('realtime', $document)); break; } catch (Throwable) { Console::warning("Collection not ready. Retrying connection ({$attempts})..."); sleep(DATABASE_RECONNECT_SLEEP); } } while (true); - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); }); /** @@ -238,7 +150,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { - Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { + Timer::tick(5000, function () use ($container, $stats, &$statsDocument, $logError, $authorization) { $payload = []; foreach ($stats as $projectId => $value) { $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); @@ -248,40 +160,43 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume } try { - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); $statsDocument ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + $authorization->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (Throwable $th) { call_user_func($logError, $th, "updateWorkerDocument"); } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } }); } }); -$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { +$server->onWorkerStart(function (int $workerId) use ($server, $container, $stats, $realtime, $logError) { Console::success('Worker ' . $workerId . ' started successfully'); $attempts = 0; $start = time(); - Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) { + $authorization = $container->get('authorization'); + + Timer::tick(5000, function () use ($server, $container, $realtime, $stats, $logError, $authorization) { /** * Sending current connections to project channels on the console project every 5 seconds. */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) { - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); $payload = []; - $list = Authorization::skip(fn () => $database->find('realtime', [ + $list = $authorization->skip(fn () => $database->find('realtime', [ Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), ])); @@ -321,8 +236,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, 'data' => $event['data'] ])); } - - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } } /** @@ -352,13 +267,26 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, while ($attempts < 300) { try { if ($attempts > 0) { - Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). - Attempting restart in 5 seconds (attempt #' . $attempts . ')'); + Console::error( + 'Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). + Attempting restart in 5 seconds (attempt #' . $attempts . ')' + ); sleep(5); // 5 sec delay between connection attempts } + $start = time(); - $redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */ + $pools = $container->get('pools'); + $pool = $pools['pools-pubsub-pubsub']['pool']; + + /** @var Connections $connections */ + $connections = $container->get('connections'); + $connection = $pool->get(); + $connections->add($connection, $pool); + + $redis = $connection; + + /** @var Redis $redis */ $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -368,7 +296,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) { + $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $realtime, $authorization, $container) { $event = json_decode($payload, true); if ($event['permissionsChanged'] && isset($event['userId'])) { @@ -377,25 +305,24 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) { $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); - $consoleDatabase = getConsoleDB(); - $project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); - $database = getProjectDB($project); + $dbForConsole = $container->get('dbForConsole'); - $user = $database->getDocument('users', $userId); + $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $dbForProject = $container->get('getProjectDB')($project); - $roles = Auth::getRoles($user); + $user = $dbForProject->getDocument('users', $userId); + + $roles = Auth::getRoles($user, $authorization); $channels = $realtime->connections[$connection]['channels']; $realtime->unsubscribe($connection); $realtime->subscribe($projectId, $connection, $roles, $channels); - - $register->get('pools')->reclaim(); } } $receivers = $realtime->getSubscribers($event); - if (App::isDevelopment() && !empty($receivers)) { + if (Http::isDevelopment() && !empty($receivers)) { Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers)); Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers)); Console::log("[Debug][Worker {$workerId}] Event: " . $payload); @@ -421,30 +348,40 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, sleep(DATABASE_RECONNECT_SLEEP); continue; } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } } Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) { - $app = new App('UTC'); - $request = new Request($request); - $response = new Response(new SwooleResponse()); +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $container, $stats, &$realtime, $logError) { + $authorization = $container->get('authorization'); + + $request = new Request(new UtopiaRequest($request)); + $response = new Response(new UtopiaResponse(new SwooleResponse())); + + $requestInjection = new Dependency(); + $responseInjection = new Dependency(); + + $requestInjection->setName('request')->setCallback(fn () => $request); + $responseInjection->setName('response')->setCallback(fn () => $response); + + $container->set($requestInjection); + $container->set($responseInjection); Console::info("Connection open (user: {$connection})"); - App::setResource('pools', fn () => $register->get('pools')); - App::setResource('request', fn () => $request); - App::setResource('response', fn () => $response); - try { + /** @var Document $project */ - $project = $app->getResource('project'); + $project = $container->refresh('project')->get('project'); + + $container->refresh('dbForProject'); /* - * Project Check + * Project Check */ if (empty($project->getId())) { throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID'); @@ -453,15 +390,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, if ( array_key_exists('realtime', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['realtime'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } - $dbForProject = getProjectDB($project); - $console = $app->getResource('console'); /** @var Document $console */ - $user = $app->getResource('user'); /** @var Document $user */ - + $dbForProject = $container->get('getProjectDB')($project); + /** @var Document $console */ + $console = $container->get('console'); + /** @var Document $user */ + $user = $container->refresh('user')->get('user'); /* * Abuse Check * @@ -490,7 +428,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription()); } - $roles = Auth::getRoles($user); + $authorization = $container->get('authorization'); + $roles = Auth::getRoles($user, $authorization); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); @@ -539,25 +478,33 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $server->send([$connection], json_encode($response)); $server->close($connection, $code); - if (App::isDevelopment()) { + if (Http::isDevelopment()) { Console::error('[Error] Connection Error'); Console::error('[Error] Code: ' . $response['data']['code']); Console::error('[Error] Message: ' . $response['data']['message']); } } finally { - $register->get('pools')->reclaim(); + $connections = $container->get('connections'); + $connections->reclaim(); } }); -$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) { +$server->onWorkerStop(function (int $workerId) use ($container) { + $connections = $container->get('connections'); + $connections->reclaim(); +}); + +$server->onMessage(function (int $connection, string $message) use ($server, $container, $realtime, $containerId) { try { - $response = new Response(new SwooleResponse()); + $response = new Response(new HttpResponse(new SwooleHttpResponse())); $projectId = $realtime->connections[$connection]['projectId']; - $database = getConsoleDB(); + $database = $container->get('dbForConsole'); + $authorization = $container->get('authorization'); + $authentication = $container->get('authentication'); if ($projectId !== 'console') { - $project = Authorization::skip(fn () => $database->getDocument('projects', $projectId)); - $database = getProjectDB($project); + $project = $authorization->skip(fn () => $database->getDocument('projects', $projectId)); + $database = $container->get('getProjectDB')($project); } else { $project = null; } @@ -595,20 +542,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re } $session = Auth::decodeSession($message['data']['session']); - Auth::$unique = $session['id'] ?? ''; - Auth::$secret = $session['secret'] ?? ''; - $user = $database->getDocument('users', Auth::$unique); + $authentication->setUnique($session['id'] ?? ''); + $authentication->setSecret($session['secret'] ?? ''); + + $user = $database->getDocument('users', $authentication->getUnique()); if ( empty($user->getId()) // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token + || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) // Validate user has valid login token ) { // cookie not valid throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.'); } - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $authorization); $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); @@ -642,7 +590,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $server->close($connection, $th->getCode()); } } finally { - $register->get('pools')->reclaim(); + ($container->get('connections'))->reclaim(); + $container->refresh('dbForConsole'); } }); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index ad35135a6f..4365bf39fc 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -11,7 +11,8 @@ $httpsPort = $this->getParam('httpsPort', ''); $version = $this->getParam('version', ''); $organization = $this->getParam('organization', ''); $image = $this->getParam('image', ''); -?>services: +?> +services: traefik: image: traefik:2.11 container_name: appwrite-traefik @@ -853,7 +854,7 @@ $image = $this->getParam('image', ''); - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync' + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' redis: image: redis:7.2.4-alpine diff --git a/app/worker.php b/app/worker.php index 6078a0127c..55e14280f6 100644 --- a/app/worker.php +++ b/app/worker.php @@ -2,278 +2,107 @@ require_once __DIR__ . '/init.php'; -use Appwrite\Event\Audit; -use Appwrite\Event\Build; -use Appwrite\Event\Certificate; -use Appwrite\Event\Database as EventDatabase; -use Appwrite\Event\Delete; -use Appwrite\Event\Event; -use Appwrite\Event\Func; -use Appwrite\Event\Mail; -use Appwrite\Event\Messaging; -use Appwrite\Event\Migration; -use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; +use Appwrite\Utopia\Queue\Connections; use Swoole\Runtime; -use Utopia\App; -use Utopia\Cache\Adapter\Sharding; -use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DSN\DSN; +use Utopia\DI\Dependency; use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Platform\Service; -use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Queue\Message; -use Utopia\Queue\Server; -use Utopia\Registry\Registry; +use Utopia\Queue\Worker; +use Utopia\Storage\Device\Local; use Utopia\System\System; -Authorization::disable(); +global $registry, $container; + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -Server::setResource('register', fn () => $register); +$project = new Dependency(); +$register = new Dependency(); +$dbForProject = new Dependency(); +$abuseRetention = new Dependency(); +$deviceForCache = new Dependency(); +$auditRetention = new Dependency(); +$queueForUsageDump = new Dependency(); +$executionRetention = new Dependency(); +$deviceForLocalFiles = new Dependency(); -Server::setResource('dbForConsole', function (Cache $cache, Registry $register) { - $pools = $register->get('pools'); - $database = $pools - ->get('console') - ->pop() - ->getResource(); +$register + ->setName('register') + ->setCallback(fn () => $registry); - $adapter = new Database($database, $cache); - $adapter->setNamespace('_console'); +$project + ->setName('project') + ->inject('message') + ->inject('dbForConsole') + ->setCallback(function (Message $message, Database $dbForConsole) { + $payload = $message->getPayload() ?? []; + $project = new Document($payload['project'] ?? []); - return $adapter; -}, ['cache', 'register']); - -Server::setResource('project', function (Message $message, Database $dbForConsole) { - $payload = $message->getPayload() ?? []; - $project = new Document($payload['project'] ?? []); - - if ($project->getId() === 'console') { - return $project; - } - - return $dbForConsole->getDocument('projects', $project->getId()); -}, ['message', 'dbForConsole']); - -Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - $pools = $register->get('pools'); - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $adapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($adapter, $cache); - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; -}, ['cache', 'register', 'message', 'project', 'dbForConsole']); - -Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; + if ($project->getId() === 'console') { + return $project; } - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } + return $dbForConsole->getDocument('projects', $project->getId()); + }); - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; +$abuseRetention + ->setName('abuseRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); + }); - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } +$auditRetention + ->setName('auditRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); + }); - return $database; - } +$executionRetention + ->setName('executionRetention') + ->setCallback(function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); + }); - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); +$queueForUsageDump + ->setName('queueForUsageDump') + ->inject('queue') + ->setCallback(function (Connection $queue) { + return new UsageDump($queue); + }); - $database = new Database($dbAdapter, $cache); +$deviceForCache + ->setName('deviceForCache') + ->inject('project') + ->setCallback(function (Document $project) { + return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); + }); - $databases[$dsn->getHost()] = $database; +$deviceForLocalFiles + ->setName('deviceForLocalFiles') + ->inject('project') + ->setCallback(function (Document $project) { + return new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); + }); - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } +$container->set($project); +$container->set($register); +$container->set($dbForProject); +$container->set($abuseRetention); +$container->set($auditRetention); +$container->set($deviceForCache); +$container->set($queueForUsageDump); +$container->set($executionRetention); +$container->set($deviceForLocalFiles); - return $database; - }; -}, ['pools', 'dbForConsole', 'cache']); - -Server::setResource('abuseRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); -}); - -Server::setResource('auditRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); -}); - -Server::setResource('executionRetention', function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); -}); - -Server::setResource('cache', function (Registry $register) { - $pools = $register->get('pools'); - $list = Config::getParam('pools-cache', []); - $adapters = []; - - foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; - } - - return new Cache(new Sharding($adapters)); -}, ['register']); - -Server::setResource('log', fn () => new Log()); - -Server::setResource('queueForUsage', function (Connection $queue) { - return new Usage($queue); -}, ['queue']); - -Server::setResource('queueForUsageDump', function (Connection $queue) { - return new UsageDump($queue); -}, ['queue']); - -Server::setResource('queue', function (Group $pools) { - return $pools->get('queue')->pop()->getResource(); -}, ['pools']); - -Server::setResource('queueForDatabase', function (Connection $queue) { - return new EventDatabase($queue); -}, ['queue']); - -Server::setResource('queueForMessaging', function (Connection $queue) { - return new Messaging($queue); -}, ['queue']); - -Server::setResource('queueForMails', function (Connection $queue) { - return new Mail($queue); -}, ['queue']); - -Server::setResource('queueForBuilds', function (Connection $queue) { - return new Build($queue); -}, ['queue']); - -Server::setResource('queueForDeletes', function (Connection $queue) { - return new Delete($queue); -}, ['queue']); - -Server::setResource('queueForEvents', function (Connection $queue) { - return new Event($queue); -}, ['queue']); - -Server::setResource('queueForAudits', function (Connection $queue) { - return new Audit($queue); -}, ['queue']); - -Server::setResource('queueForFunctions', function (Connection $queue) { - return new Func($queue); -}, ['queue']); - -Server::setResource('queueForCertificates', function (Connection $queue) { - return new Certificate($queue); -}, ['queue']); - -Server::setResource('queueForMigrations', function (Connection $queue) { - return new Migration($queue); -}, ['queue']); - -Server::setResource('logger', function (Registry $register) { - return $register->get('logger'); -}, ['register']); - -Server::setResource('pools', function (Registry $register) { - return $register->get('pools'); -}, ['register']); - -Server::setResource('deviceForFunctions', function (Document $project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForFiles', function (Document $project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForBuilds', function (Document $project) { - return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); -}, ['project']); - -Server::setResource('deviceForCache', function (Document $project) { - return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); -}, ['project']); - - -$pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); @@ -292,6 +121,13 @@ if (\str_starts_with($workerName, 'databases')) { } try { + $connection = new Connection\Redis( + System::getEnv('_APP_REDIS_HOST', 'redis'), + System::getEnv('_APP_REDIS_PORT', '6379'), + System::getEnv('_APP_REDIS_USER', ''), + System::getEnv('_APP_REDIS_PASS', '') + ); + /** * Any worker can be configured with the following env vars: * - _APP_WORKERS_NUM The total number of worker processes @@ -300,32 +136,35 @@ try { */ $platform->init(Service::TYPE_WORKER, [ 'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1), - 'connection' => $pools->get('queue')->pop()->getResource(), + 'connection' => $connection, 'workerName' => strtolower($workerName) ?? null, 'queueName' => $queueName ]); } catch (\Throwable $e) { - Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); + Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); } -$worker = $platform->getWorker(); - -$worker - ->shutdown() - ->inject('pools') - ->action(function (Group $pools) { - $pools->reclaim(); +Worker::init() + ->inject('authorization') + ->action(function (Authorization $authorization) { + $authorization->disable(); }); -$worker - ->error() +Worker::shutdown() + ->inject('connections') + ->action(function (Connections $connections) { + $connections->reclaim(); + }); + +Worker::error() ->inject('error') ->inject('logger') ->inject('log') - ->inject('pools') + ->inject('connections') ->inject('project') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) { - $pools->reclaim(); + ->inject('authorization') + ->action(function (Throwable $error, ?Logger $logger, Log $log, Connections $connections, Document $project, Authorization $authorization) use ($queueName) { + $connections->reclaim(); $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { @@ -341,7 +180,7 @@ $worker $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', Authorization::getRoles()); + $log->addExtra('roles', $authorization->getRoles()); $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); @@ -360,9 +199,7 @@ $worker Console::error('[Error] Line: ' . $error->getLine()); }); -$worker->workerStart() - ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); - }); - -$worker->start(); +$platform + ->getWorker() + ->setContainer($container) + ->start(); diff --git a/composer.json b/composer.json index 26b8c3a7a3..e23806adc9 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "description": "End to end backend server for frontend and mobile apps.", "type": "project", "license": "BSD-3-Clause", + "minimum-stability": "stable", "authors": [ { "name": "Eldad Fux", @@ -14,7 +15,8 @@ "test": "vendor/bin/phpunit", "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "bench": "vendor/bin/phpbench run --report=benchmark" + "bench": "vendor/bin/phpbench run --report=benchmark", + "check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G app src tests" }, "autoload": { "psr-4": { @@ -45,33 +47,33 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.15.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.43.0", - "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.43.0", + "utopia-php/abuse": "0.44.*", + "utopia-php/analytics": "0.13.*", + "utopia-php/audit": "0.44.*", "utopia-php/cache": "0.10.*", - "utopia-php/cli": "0.15.*", + "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.53.5-rc1", - "utopia-php/domains": "0.5.*", - "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "0.33.*", + "utopia-php/database": "0.54.*", + "utopia-php/domains": "0.6.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "1.0.*", "utopia-php/fetch": "0.2.*", "utopia-php/image": "0.6.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.5.*", - "utopia-php/orchestration": "0.9.*", - "utopia-php/platform": "0.7.*", + "utopia-php/migration": "0.4.*", + "utopia-php/orchestration": "0.15.*", + "utopia-php/platform": "0.8.*", + "utopia-php/view": "0.2.*", "utopia-php/pools": "0.5.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.7.*", + "utopia-php/queue": "0.8.*", "utopia-php/registry": "0.5.*", - "utopia-php/storage": "0.18.*", - "utopia-php/swoole": "0.8.*", + "utopia-php/storage": "0.19.*", "utopia-php/system": "0.8.*", - "utopia-php/vcs": "0.8.*", - "utopia-php/websocket": "0.1.*", + "utopia-php/vcs": "0.9.*", + "utopia-php/websocket": "0.2.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.9.1", @@ -88,7 +90,8 @@ "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.7", "laravel/pint": "^1.14", - "phpbench/phpbench": "^1.2" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "1.8.*" }, "provide": { "ext-phpiredis": "*" @@ -97,11 +100,5 @@ "platform": { "php": "8.3" } - }, - "repositories": { - "utopia-php/database": { - "type": "vcs", - "url": "https://github.com/utopia-php/database" - } } } diff --git a/composer.lock b/composer.lock index b3a94545b4..cff13d28a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7066d9ca32e7a1a60614effdc4701970", + "content-hash": "6017f815da50b7d4dabad66386e013e3", "packages": [ { "name": "adhocore/jwt", @@ -1429,16 +1429,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.43.0", + "version": "0.44.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6" + "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", + "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", "shasum": "" }, "require": { @@ -1446,7 +1446,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.53.*" + "utopia-php/database": "0.54.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1474,27 +1474,28 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.43.0" + "source": "https://github.com/utopia-php/abuse/tree/0.44.0" }, - "time": "2024-08-30T05:17:23+00:00" + "time": "2024-09-05T16:09:32+00:00" }, { "name": "utopia-php/analytics", - "version": "0.10.2", + "version": "0.13.0", "source": { "type": "git", "url": "https://github.com/utopia-php/analytics.git", - "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f" + "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/analytics/zipball/14c805114736f44c26d6d24b176a2f8b93d86a1f", - "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f", + "url": "https://api.github.com/repos/utopia-php/analytics/zipball/3dace02af5d4190623f88fb6e02f5559a99f14c4", + "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "^0.15.0" + "utopia-php/cli": "0.19.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "dev-main", @@ -1520,27 +1521,27 @@ ], "support": { "issues": "https://github.com/utopia-php/analytics/issues", - "source": "https://github.com/utopia-php/analytics/tree/0.10.2" + "source": "https://github.com/utopia-php/analytics/tree/0.13.0" }, - "time": "2023-03-22T12:01:09+00:00" + "time": "2024-09-05T16:19:26+00:00" }, { "name": "utopia-php/audit", - "version": "0.43.0", + "version": "0.44.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e" + "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/69eee24e4d6cb8fdae31235d80b9a46b18092139", + "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.53.*" + "utopia-php/database": "0.54.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1567,9 +1568,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.43.0" + "source": "https://github.com/utopia-php/audit/tree/0.44.0" }, - "time": "2024-08-30T05:17:36+00:00" + "time": "2024-09-05T16:12:41+00:00" }, { "name": "utopia-php/cache", @@ -1623,27 +1624,29 @@ }, { "name": "utopia-php/cli", - "version": "0.15.0", + "version": "0.19.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea" + "reference": "f8af1d6087f498bc1f0191750a118d357ded9948" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/f8af1d6087f498bc1f0191750a118d357ded9948", + "reference": "f8af1d6087f498bc1f0191750a118d357ded9948", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/framework": "0.*.*" + "utopia-php/di": "0.1.*", + "utopia-php/framework": "1.0.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.6", - "vimeo/psalm": "4.0.1" + "swoole/ide-helper": "4.8.8" }, "type": "library", "autoload": { @@ -1666,9 +1669,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.15.0" + "source": "https://github.com/utopia-php/cli/tree/0.19.0" }, - "time": "2023-03-01T05:55:14+00:00" + "time": "2024-09-05T15:46:56+00:00" }, { "name": "utopia-php/config", @@ -1723,16 +1726,16 @@ }, { "name": "utopia-php/database", - "version": "0.53.5-rc1", + "version": "0.54.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d" + "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/689ba22063bf46def385da8695ba7a921e81e38d", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d", + "url": "https://api.github.com/repos/utopia-php/database/zipball/c32d6eab5992c927cbf6fb4aad51d76fc5f64946", + "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946", "shasum": "" }, "require": { @@ -1740,7 +1743,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.10.*", - "utopia-php/framework": "0.33.*", + "utopia-php/framework": "1.0.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1751,7 +1754,7 @@ "phpunit/phpunit": "9.6.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.14.*" + "utopia-php/cli": "0.19.*" }, "type": "library", "autoload": { @@ -1759,38 +1762,7 @@ "Utopia\\Database\\": "src/Database" } }, - "autoload-dev": { - "psr-4": { - "Tests\\E2E\\": "tests/e2e", - "Tests\\Unit\\": "tests/unit" - } - }, - "scripts": { - "build": [ - "Composer\\Config::disableProcessTimeout", - "docker compose build" - ], - "start": [ - "Composer\\Config::disableProcessTimeout", - "docker compose up -d" - ], - "test": [ - "Composer\\Config::disableProcessTimeout", - "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml" - ], - "lint": [ - "./vendor/bin/pint --test" - ], - "format": [ - "./vendor/bin/pint" - ], - "check": [ - "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M" - ], - "coverage": [ - "./vendor/bin/coverage-check ./tmp/clover.xml 90" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -1803,31 +1775,80 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/0.53.5-rc1", - "issues": "https://github.com/utopia-php/database/issues" + "issues": "https://github.com/utopia-php/database/issues", + "source": "https://github.com/utopia-php/database/tree/0.54.1" }, - "time": "2024-09-24T08:43:10+00:00" + "time": "2024-09-10T10:08:37+00:00" }, { - "name": "utopia-php/domains", - "version": "0.5.0", + "name": "utopia-php/di", + "version": "0.1.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/domains.git", - "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c" + "url": "https://github.com/utopia-php/di.git", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c", - "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c", + "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.1.0" + }, + "time": "2024-08-08T14:35:19+00:00" + }, + { + "name": "utopia-php/domains", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/domains.git", + "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/5c70b0f524deeb1fccc3962ad1da650ae2c94cda", + "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/framework": "0.*.*" + "utopia-php/framework": "1.0.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.3" }, "type": "library", @@ -1864,9 +1885,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.5.0" + "source": "https://github.com/utopia-php/domains/tree/0.6.0" }, - "time": "2024-01-03T22:04:27+00:00" + "time": "2024-09-05T16:21:11+00:00" }, { "name": "utopia-php/dsn", @@ -1956,26 +1977,30 @@ }, { "name": "utopia-php/framework", - "version": "0.33.8", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" + "reference": "fc63ec61c720190a5ea5bb484c615145850951e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", + "url": "https://api.github.com/repos/utopia-php/http/zipball/fc63ec61c720190a5ea5bb484c615145850951e6", + "reference": "fc63ec61c720190a5ea5bb484c615145850951e6", "shasum": "" }, "require": { - "php": ">=8.0" + "ext-swoole": "*", + "php": ">=8.0", + "utopia-php/servers": "0.1.*" }, "require-dev": { + "ext-xdebug": "*", "laravel/pint": "^1.2", "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25" + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" }, "type": "library", "autoload": { @@ -1987,17 +2012,18 @@ "license": [ "MIT" ], - "description": "A simple, light and advanced PHP framework", + "description": "A simple, light and advanced PHP HTTP framework", "keywords": [ "framework", + "http", "php", "upf" ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.8" + "source": "https://github.com/utopia-php/http/tree/1.0.2" }, - "time": "2024-08-15T14:10:09+00:00" + "time": "2024-09-10T09:04:19+00:00" }, { "name": "utopia-php/image", @@ -2100,16 +2126,16 @@ }, { "name": "utopia-php/logger", - "version": "0.6.1", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c" + "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/7e8ff512c6f04577aba1df67c7b9628971946f9c", - "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", + "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", "shasum": "" }, "require": { @@ -2148,9 +2174,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.6.1" + "source": "https://github.com/utopia-php/logger/tree/0.6.0" }, - "time": "2024-09-20T14:02:12+00:00" + "time": "2024-05-23T13:37:54+00:00" }, { "name": "utopia-php/messaging", @@ -2205,16 +2231,16 @@ }, { "name": "utopia-php/migration", - "version": "0.5.2", + "version": "0.4.4", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a" + "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f18d44d4459f78c292dac9edde856fd156fe497a", - "reference": "f18d44d4459f78c292dac9edde856fd156fe497a", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/a8a5d392bebf082faf289f4dfe09d9fd76844c33", + "reference": "a8a5d392bebf082faf289f4dfe09d9fd76844c33", "shasum": "" }, "require": { @@ -2224,7 +2250,6 @@ "require-dev": { "laravel/pint": "1.*", "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", "vlucas/phpdotenv": "5.*" }, "type": "library", @@ -2247,9 +2272,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.2" + "source": "https://github.com/utopia-php/migration/tree/0.4.4" }, - "time": "2024-07-22T09:27:07+00:00" + "time": "2024-05-17T05:25:31+00:00" }, { "name": "utopia-php/mongo", @@ -2313,26 +2338,26 @@ }, { "name": "utopia-php/orchestration", - "version": "0.9.1", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/utopia-php/orchestration.git", - "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0" + "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/55f43513b3f940a3f4f9c2cde7682d0c2581beb0", - "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0", + "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/cd55650ba5f13118c3580048e6dd86b604f9a5b3", + "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.15.*" + "utopia-php/cli": "0.19.*" }, "require-dev": { "laravel/pint": "^1.2", - "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -2357,31 +2382,32 @@ ], "support": { "issues": "https://github.com/utopia-php/orchestration/issues", - "source": "https://github.com/utopia-php/orchestration/tree/0.9.1" + "source": "https://github.com/utopia-php/orchestration/tree/0.15.0" }, - "time": "2023-03-17T15:05:06+00:00" + "time": "2024-09-05T16:28:02+00:00" }, { "name": "utopia-php/platform", - "version": "0.7.0", + "version": "0.8.1", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" + "reference": "95d57f38a4001c7189a66885c485ac635d305234" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/95d57f38a4001c7189a66885c485ac635d305234", + "reference": "95d57f38a4001c7189a66885c485ac635d305234", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.7.*" + "utopia-php/cli": "0.19.*", + "utopia-php/framework": "1.0.*", + "utopia-php/queue": "0.8.*", + "utopia-php/servers": "0.1.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2407,9 +2433,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.0" + "source": "https://github.com/utopia-php/platform/tree/0.8.1" }, - "time": "2024-05-08T17:00:55+00:00" + "time": "2024-09-06T02:33:27+00:00" }, { "name": "utopia-php/pools", @@ -2517,22 +2543,23 @@ }, { "name": "utopia-php/queue", - "version": "0.7.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "917565256eb94bcab7246f7a746b1a486813761b" + "reference": "a518b271f8c158d6e66e36972f767189111033c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/917565256eb94bcab7246f7a746b1a486813761b", - "reference": "917565256eb94bcab7246f7a746b1a486813761b", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/a518b271f8c158d6e66e36972f767189111033c2", + "reference": "a518b271f8c158d6e66e36972f767189111033c2", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.*.*" + "utopia-php/cli": "0.19.*", + "utopia-php/di": "0.1.*", + "utopia-php/servers": "0.1.*" }, "require-dev": { "laravel/pint": "^0.2.3", @@ -2542,6 +2569,7 @@ "workerman/workerman": "^4.0" }, "suggest": { + "ext-redis": "Needed to support Redis connections", "ext-swoole": "Needed to support Swoole.", "workerman/workerman": "Needed to support Workerman." }, @@ -2572,9 +2600,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.7.0" + "source": "https://github.com/utopia-php/queue/tree/0.8.0" }, - "time": "2024-01-17T19:00:43+00:00" + "time": "2024-09-05T16:33:01+00:00" }, { "name": "utopia-php/registry", @@ -2629,17 +2657,70 @@ "time": "2021-03-10T10:45:22+00:00" }, { - "name": "utopia-php/storage", - "version": "0.18.5", + "name": "utopia-php/servers", + "version": "0.1.1", "source": { "type": "git", - "url": "https://github.com/utopia-php/storage.git", - "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919" + "url": "https://github.com/utopia-php/servers.git", + "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/7d355c5e3ccc8ecebc0266f8ddd30088a43be919", - "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/fd5c8d32778f265256c1936372a071b944f5ba8a", + "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "utopia-php/di": "0.1.*" + }, + "require-dev": { + "laravel/pint": "^0.2.3", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Servers\\": "src/Servers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "description": "A base library for building Utopia style servers.", + "keywords": [ + "framework", + "php", + "servers", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/servers/issues", + "source": "https://github.com/utopia-php/servers/tree/0.1.1" + }, + "time": "2024-09-06T02:25:56+00:00" + }, + { + "name": "utopia-php/storage", + "version": "0.19.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/storage.git", + "reference": "5013b894a776874d6010753fc9349df2225c69af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/5013b894a776874d6010753fc9349df2225c69af", + "reference": "5013b894a776874d6010753fc9349df2225c69af", "shasum": "" }, "require": { @@ -2651,8 +2732,8 @@ "ext-zlib": "*", "ext-zstd": "*", "php": ">=8.0", - "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.*.*" + "utopia-php/framework": "1.0.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2679,60 +2760,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.5" + "source": "https://github.com/utopia-php/storage/tree/0.19.0" }, - "time": "2024-09-04T08:57:27+00:00" - }, - { - "name": "utopia-php/swoole", - "version": "0.8.2", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/swoole.git", - "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", - "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", - "shasum": "" - }, - "require": { - "ext-swoole": "*", - "php": ">=8.0", - "utopia-php/framework": "0.33.*" - }, - "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "5.0.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Swoole\\": "src/Swoole" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "An extension for Utopia Framework to work with PHP Swoole as a PHP FPM alternative", - "keywords": [ - "framework", - "http", - "php", - "server", - "swoole", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.8.2" - }, - "time": "2024-02-01T14:54:12+00:00" + "time": "2024-09-05T17:00:24+00:00" }, { "name": "utopia-php/system", @@ -2792,23 +2822,24 @@ }, { "name": "utopia-php/vcs", - "version": "0.8.2", + "version": "0.9.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18" + "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", - "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/673abe2fef0750a841a4fa8fa6f99d4a602c68e7", + "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "^0.10.0", - "utopia-php/framework": "0.*.*" + "utopia-php/cache": "0.10.*", + "utopia-php/framework": "1.0.*", + "utopia-php/system": "0.8.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2835,32 +2866,76 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.8.2" + "source": "https://github.com/utopia-php/vcs/tree/0.9.0" }, - "time": "2024-08-13T14:36:30+00:00" + "time": "2024-09-05T16:44:48+00:00" }, { - "name": "utopia-php/websocket", - "version": "0.1.0", + "name": "utopia-php/view", + "version": "0.2.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/websocket.git", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" + "url": "https://github.com/utopia-php/view.git", + "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", - "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", + "url": "https://api.github.com/repos/utopia-php/view/zipball/6ee55e83bc014c39ed6b69390f6d399116f65e88", + "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88", "shasum": "" }, "require": { "php": ">=8.0" }, "require-dev": { + "laravel/pint": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\View\\": "src/View" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP rendering engine", + "keywords": [ + "php", + "view" + ], + "support": { + "issues": "https://github.com/utopia-php/view/issues", + "source": "https://github.com/utopia-php/view/tree/0.2.0" + }, + "time": "2024-04-01T17:21:29+00:00" + }, + { + "name": "utopia-php/websocket", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/websocket.git", + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/e9d0919b321744a61f12563f5791c47ba9f57810", + "reference": "e9d0919b321744a61f12563f5791c47ba9f57810", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.15", + "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.5.5", - "swoole/ide-helper": "4.6.6", + "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.2", - "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, "type": "library", @@ -2873,16 +2948,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Torsten Dittmann", - "email": "torsten@appwrite.io" - } - ], "description": "A simple abstraction for WebSocket servers.", "keywords": [ "framework", @@ -2893,9 +2958,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.1.0" + "source": "https://github.com/utopia-php/websocket/tree/0.2.0" }, - "time": "2021-12-20T10:50:09+00:00" + "time": "2024-04-09T08:28:11+00:00" }, { "name": "webmozart/assert", @@ -4216,16 +4281,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.31.0", + "version": "1.30.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17" + "reference": "51b95ec8670af41009e2b2b56873bad96682413e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", + "reference": "51b95ec8670af41009e2b2b56873bad96682413e", "shasum": "" }, "require": { @@ -4257,9 +4322,68 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" }, - "time": "2024-09-22T11:32:18+00:00" + "time": "2024-09-07T20:13:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.8.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "46e223dd68a620da18855c23046ddb00940b4014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", + "reference": "46e223dd68a620da18855c23046ddb00940b4014", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.11" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-10-24T15:45:13+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5896,16 +6020,16 @@ }, { "name": "symfony/console", - "version": "v7.1.5", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", "shasum": "" }, "require": { @@ -5969,7 +6093,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.5" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -5985,7 +6109,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6056,16 +6180,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.5", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", - "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", "shasum": "" }, "require": { @@ -6102,7 +6226,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.5" + "source": "https://github.com/symfony/filesystem/tree/v7.1.2" }, "funding": [ { @@ -6118,7 +6242,7 @@ "type": "tidelift" } ], - "time": "2024-09-17T09:16:35+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/finder", @@ -6567,16 +6691,16 @@ }, { "name": "symfony/process", - "version": "v7.1.5", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5c03ee6369281177f07f7c68252a280beccba847" + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", - "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", "shasum": "" }, "require": { @@ -6608,7 +6732,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.5" + "source": "https://github.com/symfony/process/tree/v7.1.3" }, "funding": [ { @@ -6624,7 +6748,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-07-26T12:44:47+00:00" }, { "name": "symfony/service-contracts", @@ -6711,16 +6835,16 @@ }, { "name": "symfony/string", - "version": "v7.1.5", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { @@ -6778,7 +6902,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.5" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -6794,7 +6918,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "textalk/websocket", @@ -7026,9 +7150,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/database": 5 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 6ecb0ecff8..c4bdffa1d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -682,6 +682,7 @@ services: entrypoint: maintenance <<: *x-logging container_name: appwrite-task-maintenance + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -782,6 +783,7 @@ services: entrypoint: schedule-functions <<: *x-logging container_name: appwrite-task-scheduler-functions + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -810,6 +812,7 @@ services: entrypoint: schedule-executions <<: *x-logging container_name: appwrite-task-scheduler-executions + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -837,6 +840,7 @@ services: entrypoint: schedule-messages <<: *x-logging container_name: appwrite-task-scheduler-messages + restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -956,7 +960,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: "mysqld --innodb-flush-method=fsync" + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' redis: image: redis:7.2.4-alpine diff --git a/docs/tutorials/add-route.md b/docs/tutorials/add-route.md index ac6fd40bdb..0baa51b5c0 100644 --- a/docs/tutorials/add-route.md +++ b/docs/tutorials/add-route.md @@ -8,7 +8,7 @@ Setting an alias allows the route to be also accessible from the alias URL. The first parameter specifies the alias URL, the second parameter specifies default values for route parameters. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ``` @@ -17,7 +17,7 @@ App::post('/v1/storage/buckets/:bucketId/files') Used as an abstract description of the route. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->desc('Create File') ``` @@ -26,14 +26,14 @@ App::post('/v1/storage/buckets/:bucketId/files') Groups array is used to group one or more routes with one or more hooks functionality. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->groups(['api']) ``` In the above example groups() is used to define the current route as part of the routes that shares a common init middleware hook. ```php -App::init() +Http::init() ->groups(['api']) ->action( some code..... @@ -52,7 +52,7 @@ Appwrite uses different labels to achieve different things, for example: - scope - Defines the route permissions scope. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('scope', 'files.write') ``` @@ -66,7 +66,7 @@ App::post('/v1/storage/buckets/:bucketId/files') - audits.resource - Signals the extraction part of the resource. ```php -App::post('/v1/account/create') +Http::post('/v1/account/create') ->label('audits.event', 'account.create') ->label('audits.resource', 'user/{response.$id}') ->label('audits.userId', '{response.$id}') @@ -84,7 +84,7 @@ App::post('/v1/account/create') * sdk.offline.response.key - JSON property name that has the ID. Defaults to $id ```php -App::post('/v1/account/jwt') +Http::post('/v1/account/jwt') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createJWT') @@ -100,7 +100,7 @@ App::post('/v1/account/jwt') - cache.resource - Identifies the cached resource. ```php -App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->label('cache', true) ->label('cache.resource', 'file/{request.fileId}') ``` @@ -115,7 +115,7 @@ When using the example below, we configure the abuse mechanism to allow this key constructed from the combination of the ip, http method, url, userId to hit the route maximum 60 times in 1 hour (60 seconds \* 60 minutes). ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}') ->label('abuse-limit', 60) ->label('abuse-time', 3600) @@ -127,7 +127,7 @@ App::post('/v1/storage/buckets/:bucketId/files') Placeholders marked as `[]` are parsed and replaced with their real values. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->label('event', 'buckets.[bucketId].files.[fileId].create') ``` @@ -145,7 +145,7 @@ As the name implies, `param()` is used to define a request parameter. - An array of injections ```php -App::get('/v1/account/logs') +Http::get('/v1/account/logs') ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ``` @@ -154,14 +154,14 @@ App::get('/v1/account/logs') inject is used to inject dependencies pre-bounded to the app. ```php -App::post('/v1/storage/buckets/:bucketId/files') +Http::post('/v1/storage/buckets/:bucketId/files') ->inject('user') ``` -In the example above, the user object is injected into the route pre-bounded using `App::setResource()`. +In the example above, the user object is injected into the route pre-bounded using `Http::setResource()`. ```php -App::setResource('user', function() { +Http::setResource('user', function() { some code... }); ``` @@ -170,7 +170,7 @@ some code... Action populates the actual route code and has to be very clear and understandable. A good route stays simple and doesn't contain complex logic. An action is where we describe our business needs in code, and combine different libraries to work together and tell our story. ```php -App::post('/v1/account/sessions/anonymous') +Http::post('/v1/account/sessions/anonymous') ->action(function (Request $request) { some code... }); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000..25771ef17c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + level: 0 + scanDirectories: + - vendor/swoole/ide-helper + excludePaths: + - tests/resources + ignoreErrors: + - '#Parameter \$geodb of anonymous function has invalid type MaxMind\\Db\\Reader\.#' + - '#Parameter \$geodb of function router\(\) has invalid type MaxMind\\Db\\Reader\.#' + - '#Instantiated class MaxMind\\Db\\Reader not found.#' + - '#Function scrypt not found\.#' diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 1e8109622e..0135020765 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -91,37 +91,6 @@ class Auth */ public const MFA_RECENT_DURATION = 1800; // 30 mins - /** - * @var string - */ - public static $cookieName = 'a_session'; - - /** - * User Unique ID. - * - * @var string - */ - public static $unique = ''; - - /** - * User Secret Key. - * - * @var string - */ - public static $secret = ''; - - /** - * Set Cookie Name. - * - * @param $string - * - * @return string - */ - public static function setCookieName($string) - { - return self::$cookieName = $string; - } - /** * Encode Session. * @@ -439,13 +408,14 @@ class Auth * Returns all roles for a user. * * @param Document $user + * @param Authorization $auth * @return array */ - public static function getRoles(Document $user): array + public static function getRoles(Document $user, Authorization $auth): array { $roles = []; - if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) { + if (!self::isPrivilegedUser($auth->getRoles()) && !self::isAppUser($auth->getRoles())) { if ($user->getId()) { $roles[] = Role::user($user->getId())->toString(); $roles[] = Role::users()->toString(); diff --git a/src/Appwrite/Auth/Authentication.php b/src/Appwrite/Auth/Authentication.php new file mode 100644 index 0000000000..ef372309da --- /dev/null +++ b/src/Appwrite/Auth/Authentication.php @@ -0,0 +1,57 @@ +cookieName = $string; + } + + public function getCookieName(): string + { + return $this->cookieName; + } + + public function getUnique(): string + { + return $this->unique; + } + + public function setUnique(string $unique): void + { + $this->unique = $unique; + } + + public function getSecret(): string + { + return $this->secret; + } + + public function setSecret(string $secret): void + { + $this->secret = $secret; + } + +} diff --git a/src/Appwrite/Auth/Validator/MockNumber.php b/src/Appwrite/Auth/Validator/MockNumber.php index ac5ba89fc5..8f0f14c9da 100644 --- a/src/Appwrite/Auth/Validator/MockNumber.php +++ b/src/Appwrite/Auth/Validator/MockNumber.php @@ -2,8 +2,8 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; -use Utopia\Validator\Text; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Text; /** * MockNumber. @@ -52,6 +52,7 @@ class MockNumber extends Validator return false; } + $this->message = ''; return true; } diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index bfe5577889..913701f7a3 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Password. diff --git a/src/Appwrite/Auth/Validator/Phone.php b/src/Appwrite/Auth/Validator/Phone.php index 26aa687278..d5f6df60c8 100644 --- a/src/Appwrite/Auth/Validator/Phone.php +++ b/src/Appwrite/Auth/Validator/Phone.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Phone. diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index 0cbaf17b60..2fbdb7e785 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -175,9 +175,9 @@ class Func extends Event * * @return string */ - public function getData(): string + public function getBody(): string { - return $this->data; + return $this->body; } /** diff --git a/src/Appwrite/Event/Validator/Event.php b/src/Appwrite/Event/Validator/Event.php index a3605e4df5..2d46bd7727 100644 --- a/src/Appwrite/Event/Validator/Event.php +++ b/src/Appwrite/Event/Validator/Event.php @@ -3,7 +3,7 @@ namespace Appwrite\Event\Validator; use Utopia\Config\Config; -use Utopia\Validator; +use Utopia\Http\Validator; class Event extends Validator { diff --git a/src/Appwrite/Functions/Validator/Headers.php b/src/Appwrite/Functions/Validator/Headers.php index 04003d535b..4c30a98045 100644 --- a/src/Appwrite/Functions/Validator/Headers.php +++ b/src/Appwrite/Functions/Validator/Headers.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Headers. diff --git a/src/Appwrite/Functions/Validator/Payload.php b/src/Appwrite/Functions/Validator/Payload.php index acb461eabd..3b2ff4b918 100644 --- a/src/Appwrite/Functions/Validator/Payload.php +++ b/src/Appwrite/Functions/Validator/Payload.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator\Text; +use Utopia\Http\Validator\Text; class Payload extends Text { diff --git a/src/Appwrite/Functions/Validator/RuntimeSpecification.php b/src/Appwrite/Functions/Validator/RuntimeSpecification.php index 22311838f6..fa6efe90a4 100644 --- a/src/Appwrite/Functions/Validator/RuntimeSpecification.php +++ b/src/Appwrite/Functions/Validator/RuntimeSpecification.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class RuntimeSpecification extends Validator { diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 8bc72af2f8..49d7c421f7 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -6,9 +6,12 @@ use Appwrite\GraphQL\Exception as GQLException; use Appwrite\Promises\Swoole; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\App; +use Utopia\DI\Container; use Utopia\Exception; -use Utopia\Route; +use Utopia\Http\Http; +use Utopia\Http\Request as UtopiaHttpRequest; +use Utopia\Http\Response as UtopiaHttpResponse; +use Utopia\Http\Route; use Utopia\System\System; class Resolvers @@ -16,24 +19,21 @@ class Resolvers /** * Create a resolver for a given API {@see Route}. * - * @param App $utopia + * @param Http $http * @param ?Route $route * @return callable */ - public static function api( - App $utopia, + public function api( + Http $http, ?Route $route, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) { - /** @var App $utopia */ - /** @var Response $response */ - /** @var Request $request */ - - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $route, $args, $context, $container, $info, $request, $response, $resolver) { $path = $route->getPath(); foreach ($args as $key => $value) { if (\str_contains($path, '/:' . $key)) { @@ -46,14 +46,13 @@ class Resolvers switch ($route->getMethod()) { case 'GET': - $request->setQueryString($args); + $request->setQuery($args); break; default: $request->setPayload($args); break; } - - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -61,20 +60,20 @@ class Resolvers /** * Create a resolver for a document in a specified database and collection with a specific method type. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param string $methodType * @return callable */ - public static function document( - App $utopia, + public function document( + Http $http, string $databaseId, string $collectionId, string $methodType, ): callable { return [self::class, 'document' . \ucfirst($methodType)]( - $utopia, + $http, $databaseId, $collectionId ); @@ -83,28 +82,28 @@ class Resolvers /** * Create a resolver for getting a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public static function documentGet( - App $utopia, + public function documentGet( + Http $http, string $databaseId, string $collectionId, callable $url, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -112,35 +111,35 @@ class Resolvers /** * Create a resolver for listing documents in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentList( - App $utopia, + public function documentList( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - $request->setQueryString($params($databaseId, $collectionId, $args)); + $request->setQuery($params($databaseId, $collectionId, $args)); $beforeResolve = function ($payload) { return $payload['documents']; }; - self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject, $beforeResolve); } ); } @@ -148,31 +147,31 @@ class Resolvers /** * Create a resolver for creating a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentCreate( - App $utopia, + public function documentCreate( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('POST'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -180,31 +179,31 @@ class Resolvers /** * Create a resolver for updating a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public static function documentUpdate( - App $utopia, + public function documentUpdate( + Http $http, string $databaseId, string $collectionId, callable $url, callable $params, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('PATCH'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } @@ -212,34 +211,34 @@ class Resolvers /** * Create a resolver for deleting a document in a specified database and collection. * - * @param App $utopia + * @param Http $http * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public static function documentDelete( - App $utopia, + public function documentDelete( + Http $http, string $databaseId, string $collectionId, callable $url, + UtopiaHttpRequest $request, + UtopiaHttpResponse $response, + Container $container, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { - $utopia = $utopia->getResource('utopia:graphql', true); - $request = $utopia->getResource('request', true); - $response = $utopia->getResource('response', true); - + $resolver = $this; + return fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { $request->setMethod('DELETE'); $request->setURI($url($databaseId, $collectionId, $args)); - self::resolve($utopia, $request, $response, $resolve, $reject); + $resolver->resolve($http, $request, $response, $container, $resolve, $reject); } ); } /** - * @param App $utopia + * @param Http $http * @param Request $request * @param Response $response * @param callable $resolve @@ -249,10 +248,11 @@ class Resolvers * @return void * @throws Exception */ - private static function resolve( - App $utopia, + private function resolve( + Http $http, Request $request, Response $response, + Container $context, callable $resolve, callable $reject, ?callable $beforeResolve = null, @@ -263,14 +263,16 @@ class Resolvers $request->removeHeader('content-type'); } - $request = clone $request; - $utopia->setResource('request', static fn () => $request); $response->setContentType(Response::CONTENT_TYPE_NULL); try { - $route = $utopia->match($request, fresh: true); + $context + ->refresh('cache') + ->refresh('dbForProject') + ->refresh('dbForConsole') + ->refresh('getProjectDb'); - $utopia->execute($route, $request, $response); + $http->run(clone $context); } catch (\Throwable $e) { if ($beforeReject) { $e = $beforeReject($e); @@ -285,10 +287,12 @@ class Resolvers if ($beforeReject) { $payload = $beforeReject($payload); } - $reject(new GQLException( - message: $payload['message'], - code: $response->getStatusCode() - )); + $reject( + new GQLException( + message: $payload['message'], + code: $response->getStatusCode() + ) + ); return; } diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index 833ea9d032..2b05f08aee 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -3,54 +3,63 @@ namespace Appwrite\GraphQL; use Appwrite\GraphQL\Types\Mapper; +use Appwrite\Utopia\Response\Models; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema as GQLSchema; -use Utopia\App; +use Utopia\DI\Container; use Utopia\Exception; -use Utopia\Route; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; +use Utopia\Http\Http; +use Utopia\Http\Request; +use Utopia\Http\Response as UtopiaHttpResponse; +use Utopia\Http\Route; class Schema { - protected static ?GQLSchema $schema = null; - protected static array $dirty = []; + protected ?GQLSchema $schema = null; + protected array $dirty = []; /** * - * @param App $utopia - * @param callable $complexity Function to calculate complexity - * @param callable $attributes Function to get attributes - * @param array $urls Array of functions to get urls for specific method types - * @param array $params Array of functions to build parameters for specific method types + * @param Http $http + * @param callable $complexity Function to calculate complexity + * @param callable $attributes Function to get attributes + * @param array $urls Array of functions to get urls for specific method types + * @param array $params Array of functions to build parameters for specific method types * @return GQLSchema * @throws Exception */ - public static function build( - App $utopia, + public function build( + Http $http, + Request $request, + UtopiaHttpResponse $response, + Container $container, callable $complexity, callable $attributes, array $urls, array $params, ): GQLSchema { - App::setResource('utopia:graphql', static function () use ($utopia) { - return $utopia; - }); - - if (!empty(self::$schema)) { - return self::$schema; + if (!empty($this->schema)) { + return $this->schema; } - $api = static::api( - $utopia, + $api = $this->api( + $http, + $request, + $response, + $container, $complexity ); - //$collections = static::collections( - // $utopia, - // $complexity, - // $attributes, - // $urls, - // $params, - //); + // $collections = $this->collections( + // $http, + // $complexity, + // $request, + // $response, + // $attributes, + // $urls, + // $params, + // ); $queries = \array_merge_recursive( $api['query'], @@ -64,7 +73,7 @@ class Schema \ksort($queries); \ksort($mutations); - return static::$schema = new GQLSchema([ + return $this->schema = new GQLSchema([ 'query' => new ObjectType([ 'name' => 'Query', 'fields' => $queries @@ -80,21 +89,23 @@ class Schema * This function iterates all API routes and builds a GraphQL * schema defining types and resolvers for all response models. * - * @param App $utopia + * @param Http $http + * @param Request $request + * @param UtopiaSwooleResponse $response * @param callable $complexity * @return array - * @throws Exception + * @throws \Exception */ - protected static function api(App $utopia, callable $complexity): array + protected function api(Http $http, Request $request, UtopiaHttpResponse $response, Container $container, callable $complexity): array { - Mapper::init($utopia - ->getResource('response') - ->getModels()); + Mapper::init(Models::getModels()); + + $mapper = new Mapper(); $queries = []; $mutations = []; - foreach ($utopia->getRoutes() as $routes) { + foreach ($http->getRoutes() as $routes) { foreach ($routes as $route) { /** @var Route $route */ @@ -106,7 +117,7 @@ class Schema continue; } - foreach (Mapper::route($utopia, $route, $complexity) as $field) { + foreach ($mapper->route($http, $route, $request, $response, $container, $complexity) as $field) { switch ($route->getMethod()) { case 'GET': $queries[$name] = $field; @@ -134,7 +145,7 @@ class Schema * Iterates all of a projects attributes and builds GraphQL * queries and mutations for the collections they make up. * - * @param App $utopia + * @param Http $http * @param callable $complexity * @param callable $attributes * @param array $urls @@ -143,7 +154,7 @@ class Schema * @throws \Exception */ protected static function collections( - App $utopia, + Http $http, callable $complexity, callable $attributes, array $urls, @@ -194,36 +205,36 @@ class Schema $queryFields[$collectionId . 'Get'] = [ 'type' => $objectType, 'args' => Mapper::args('id'), - 'resolve' => Resolvers::documentGet( - $utopia, + /*'resolve' => Resolvers::documentGet( + $http, $databaseId, $collectionId, $urls['get'], - ) + )*/ ]; $queryFields[$collectionId . 'List'] = [ 'type' => Type::listOf($objectType), 'args' => Mapper::args('list'), - 'resolve' => Resolvers::documentList( - $utopia, + /*'resolve' => Resolvers::documentList( + $http, $databaseId, $collectionId, $urls['list'], $params['list'], - ), + ),*/ 'complexity' => $complexity, ]; $mutationFields[$collectionId . 'Create'] = [ 'type' => $objectType, 'args' => $attributes, - 'resolve' => Resolvers::documentCreate( - $utopia, + /*'resolve' => Resolvers::documentCreate( + $http, $databaseId, $collectionId, $urls['create'], $params['create'], - ) + )*/ ]; $mutationFields[$collectionId . 'Update'] = [ 'type' => $objectType, @@ -234,23 +245,23 @@ class Schema $attributes ) ), - 'resolve' => Resolvers::documentUpdate( - $utopia, + /*'resolve' => Resolvers::documentUpdate( + $http, $databaseId, $collectionId, $urls['update'], $params['update'], - ) + )*/ ]; $mutationFields[$collectionId . 'Delete'] = [ 'type' => Mapper::model('none'), 'args' => Mapper::args('id'), - 'resolve' => Resolvers::documentDelete( - $utopia, + /*'resolve' => Resolvers::documentDelete( + $http, $databaseId, $collectionId, $urls['delete'], - ) + )*/ ]; } $offset += $limit; @@ -262,8 +273,8 @@ class Schema ]; } - public static function setDirty(string $projectId): void + public function setDirty(string $projectId): void { - self::$dirty[$projectId] = true; + $this->dirty[$projectId] = true; } } diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index 36b246b28b..a15c6aa475 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -8,10 +8,13 @@ use Exception; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use Utopia\App; -use Utopia\Route; -use Utopia\Validator; -use Utopia\Validator\Nullable; +use Utopia\DI\Container; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; +use Utopia\Http\Http; +use Utopia\Http\Request; +use Utopia\Http\Route; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Nullable; class Mapper { @@ -74,12 +77,15 @@ class Mapper return self::$args[$key] ?? []; } - public static function route( - App $utopia, + public function route( + Http $http, Route $route, + Request $request, + UtopiaSwooleResponse $response, + Container $container, callable $complexity ): iterable { - foreach (self::$blacklist as $blacklist) { + foreach (static::$blacklist as $blacklist) { if (\str_starts_with($route->getPath(), $blacklist)) { return; } @@ -101,7 +107,7 @@ class Mapper $list = true; } $parameterType = Mapper::param( - $utopia, + $container, $parameter['validator'], !$parameter['optional'], $parameter['injections'] @@ -116,7 +122,7 @@ class Mapper 'type' => $type, 'description' => $description, 'args' => $params, - 'resolve' => Resolvers::api($utopia, $route) + 'resolve' => (new Resolvers())->api($http, $route, $request, $response, $container) ]; if ($list) { @@ -205,7 +211,7 @@ class Mapper /** * Map a {@see Route} parameter to a GraphQL Type * - * @param App $utopia + * @param Container $container * @param Validator|callable $validator * @param bool $required * @param array $injections @@ -213,13 +219,13 @@ class Mapper * @throws Exception */ public static function param( - App $utopia, + Container $container, Validator|callable $validator, bool $required, array $injections ): Type { $validator = \is_callable($validator) - ? \call_user_func_array($validator, $utopia->getResources($injections)) + ? \call_user_func_array($validator, array_map(fn ($injection) => $container->get($injection), $injections)) : $validator; $isNullable = $validator instanceof Nullable; @@ -232,20 +238,20 @@ class Mapper case 'Appwrite\Network\Validator\CNAME': case 'Appwrite\Task\Validator\Cron': case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Utopia\Validator\Domain': + case 'Utopia\Http\Validator\Domain': case 'Appwrite\Network\Validator\Email': case 'Appwrite\Event\Validator\Event': case 'Appwrite\Event\Validator\FunctionEvent': - case 'Utopia\Validator\HexColor': - case 'Utopia\Validator\Host': - case 'Utopia\Validator\IP': + case 'Utopia\Http\Validator\HexColor': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\IP': case 'Utopia\Database\Validator\Key': - case 'Utopia\Validator\Origin': + case 'Utopia\Http\Validator\Origin': case 'Appwrite\Auth\Validator\Password': - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\URL': - case 'Utopia\Validator\WhiteList': + case 'Utopia\Http\Validator\URL': + case 'Utopia\Http\Validator\WhiteList': default: $type = Type::string(); break; @@ -273,29 +279,29 @@ class Mapper case 'Appwrite\Utopia\Database\Validator\Queries\Variables': $type = Type::listOf(Type::string()); break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $type = Type::boolean(); break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': $type = Type::listOf(self::param( - $utopia, + $container, $validator->getValidator(), $required, $injections )); break; - case 'Utopia\Validator\Integer': - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Range': $type = Type::int(); break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $type = Type::float(); break; - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\Assoc': $type = Types::assoc(); break; - case 'Utopia\Validator\JSON': + case 'Utopia\Http\Validator\JSON': $type = Types::json(); break; case 'Utopia\Storage\Validator\File': diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index cee1b2d263..5b215ec04f 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -98,10 +98,10 @@ abstract class Migration */ protected array $collections; - public function __construct() + public function __construct(Authorization $auth) { - Authorization::disable(); - Authorization::setDefaultStatus(false); + $auth->disable(); + $auth->setDefaultStatus(false); $this->collections = Config::getParam('collections', []); @@ -176,25 +176,23 @@ abstract class Migration Console::log('Migrating Collection ' . $collection['$id'] . ':'); foreach ($this->documentsIterator($collection['$id']) as $document) { - go(function (Document $document, callable $callback) { - if (empty($document->getId()) || empty($document->getCollection())) { - return; - } + if (empty($document->getId()) || empty($document->getCollection())) { + return; + } - $old = $document->getArrayCopy(); - $new = call_user_func($callback, $document); + $old = $document->getArrayCopy(); + $new = call_user_func($callback, $document); - if (is_null($new) || $new->getArrayCopy() == $old) { - return; - } + if (is_null($new) || $new->getArrayCopy() == $old) { + return; + } - try { - $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); - } catch (\Throwable $th) { - Console::error('Failed to update document: ' . $th->getMessage()); - return; - } - }, $document, $callback); + try { + $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); + } catch (\Throwable $th) { + Console::error('Failed to update document: ' . $th->getMessage()); + return; + } } } } diff --git a/src/Appwrite/Network/Validator/CNAME.php b/src/Appwrite/Network/Validator/CNAME.php index e1ae061c84..e9e2b586a5 100644 --- a/src/Appwrite/Network/Validator/CNAME.php +++ b/src/Appwrite/Network/Validator/CNAME.php @@ -2,7 +2,7 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class CNAME extends Validator { diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php index 3209a4aada..bae0ff0bbf 100644 --- a/src/Appwrite/Network/Validator/Email.php +++ b/src/Appwrite/Network/Validator/Email.php @@ -2,14 +2,14 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; /** * Email * * Validate that an variable is a valid email address * - * @package Utopia\Validator + * @package Utopia\Http\Validator */ class Email extends Validator { diff --git a/src/Appwrite/Network/Validator/Origin.php b/src/Appwrite/Network/Validator/Origin.php index d41e9af2ad..573a59b844 100644 --- a/src/Appwrite/Network/Validator/Origin.php +++ b/src/Appwrite/Network/Validator/Origin.php @@ -2,8 +2,8 @@ namespace Appwrite\Network\Validator; -use Utopia\Validator; -use Utopia\Validator\Hostname; +use Utopia\Http\Validator; +use Utopia\Http\Validator\Hostname; class Origin extends Validator { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 82d1ca2d59..3bf9e0d33b 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -3,13 +3,17 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; -use Utopia\App; +use Appwrite\Utopia\Queue\Connections; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Domains\Domain; use Utopia\DSN\DSN; +use Utopia\Http\Http; use Utopia\Logger\Logger; use Utopia\Platform\Action; +use Utopia\Queue\Connection\Redis; use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; @@ -27,10 +31,11 @@ class Doctor extends Action $this ->desc('Validate server health') ->inject('register') - ->callback(fn (Registry $register) => $this->action($register)); + ->inject('connections') + ->callback(fn (Registry $register, Connections $connections) => $this->action($register, $connections)); } - public function action(Registry $register): void + public function action(Registry $register, Connections $connections): void { Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ @@ -125,22 +130,41 @@ class Doctor extends Action //throw $th; } - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var array $pools */ + $pools = $register->get('pools'); $configs = [ - 'Console.DB' => Config::getParam('pools-console'), - 'Projects.DB' => Config::getParam('pools-database'), + 'Console.DB' => [ + 'prefix' => 'console', + 'databases' => Config::getParam('pools-console') + ], + 'Database.DB' => [ + 'prefix' => 'database', + 'databases' => Config::getParam('pools-database') + ], ]; + foreach ($configs as $key => $config) { - foreach ($config as $database) { + foreach ($config['databases'] as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; + $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; + + $connection = $pool->get(); + $connections->add($connection, $pool); + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + $adapter->setDatabase($dsn->getPath()); + if ($adapter->ping()) { - Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); + Console::success('🟢 ' . str_pad("$key({$database})", 50, '.') . 'connected'); } else { - Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("$key({$database})", 47, '.') . 'disconnected'); } } catch (\Throwable $th) { Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected'); @@ -148,25 +172,39 @@ class Doctor extends Action } } - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var array $pools */ + $pools = $register->get('pools'); $configs = [ - 'Cache' => Config::getParam('pools-cache'), - 'Queue' => Config::getParam('pools-queue'), - 'PubSub' => Config::getParam('pools-pubsub'), + 'Cache' => [ + 'prefix' => 'cache', + 'databases' => Config::getParam('pools-cache') + ], + 'Queue' => [ + 'prefix' => 'queue', + 'databases' => Config::getParam('pools-queue') + ], + 'PubSub' => [ + 'prefix' => 'pubsub', + 'databases' => Config::getParam('pools-pubsub') + ], ]; - foreach ($configs as $key => $config) { - foreach ($config as $pool) { + foreach ($config['databases'] as $database) { try { - $adapter = $pools->get($pool)->pop()->getResource(); + $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; + $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = new Redis($dsn->getHost(), $dsn->getPort()); if ($adapter->ping()) { - Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected'); + Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); } else { - Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } } catch (\Throwable $th) { - Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } } } @@ -258,7 +296,7 @@ class Doctor extends Action } try { - if (App::isProduction()) { + if (Http::isProduction()) { Console::log(''); $version = \json_decode(@\file_get_contents(System::getEnv('_APP_HOME', 'http://localhost') . '/version'), true); diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 4abd267684..a1a73e6385 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -8,9 +8,9 @@ use Appwrite\Docker\Env; use Appwrite\Utopia\View; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Text; use Utopia\Platform\Action; -use Utopia\Validator\Boolean; -use Utopia\Validator\Text; class Install extends Action { @@ -213,8 +213,7 @@ class Install extends Action } $env = ''; - $stdout = ''; - $stderr = ''; + $output = ''; foreach ($input as $key => $value) { if ($value) { @@ -225,13 +224,13 @@ class Install extends Action $exit = 0; if (!$noStart) { Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); - $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $output); } if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; Console::error($message); - Console::error($stderr); + Console::error($output); Console::exit($exit); } else { $message = 'Appwrite installed successfully'; diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index dcba59bb1d..55be0b81c2 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -10,10 +10,10 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Http\Validator\Text; use Utopia\Platform\Action; use Utopia\Registry\Registry; use Utopia\System\System; -use Utopia\Validator\Text; class Migrate extends Action { @@ -30,12 +30,15 @@ class Migrate extends Action ->desc('Migrate Appwrite to new version') /** @TODO APP_VERSION_STABLE needs to be defined */ ->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true) + ->inject('cache') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('register') - ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) { - \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) { - $this->action($version, $dbForConsole, $getProjectDB, $register); + ->inject('authorization') + ->inject('console') + ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register, Authorization $authorization, Document $console) { + \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register, $authorization, $console) { + $this->action($version, $dbForConsole, $getProjectDB, $register, $authorization, $console); }); }); } @@ -58,13 +61,12 @@ class Migrate extends Action } } - public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register) + public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register, Authorization $auth, Document $console) { - Authorization::disable(); + $auth->disable(); if (!array_key_exists($version, Migration::$versions)) { Console::error("Version {$version} not found."); Console::exit(1); - return; } @@ -77,12 +79,8 @@ class Migrate extends Action 10 ); - $app = new App('UTC'); - Console::success('Starting Data Migration to version ' . $version); - $console = $app->getResource('console'); - $limit = 30; $sum = 30; $offset = 0; @@ -101,7 +99,7 @@ class Migrate extends Action $class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version]; /** @var Migration $migration */ - $migration = new $class(); + $migration = new $class($auth, ); while (!empty($projects)) { foreach ($projects as $project) { diff --git a/src/Appwrite/Platform/Tasks/QueueCount.php b/src/Appwrite/Platform/Tasks/QueueCount.php index b02165c1d2..63f829073a 100644 --- a/src/Appwrite/Platform/Tasks/QueueCount.php +++ b/src/Appwrite/Platform/Tasks/QueueCount.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; class QueueCount extends Action { diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index b6139dc177..63f6c8e11e 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\Wildcard; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; -use Utopia\Validator\Text; -use Utopia\Validator\Wildcard; class QueueRetry extends Action { diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 5af0cb6cd8..ad4098d9ee 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -5,10 +5,10 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Certificate; use Utopia\CLI\Console; use Utopia\Database\Document; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Hostname; use Utopia\Platform\Action; use Utopia\System\System; -use Utopia\Validator\Boolean; -use Utopia\Validator\Hostname; class SSL extends Action { diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 79a05dcd13..8dc037897d 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -2,44 +2,44 @@ namespace Appwrite\Platform\Tasks; +use Appwrite\Utopia\Queue\Connections; use Swoole\Timer; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; use Utopia\Platform\Action; -use Utopia\Pools\Group; use Utopia\System\System; -use function Swoole\Coroutine\run; - abstract class ScheduleBase extends Action { protected const UPDATE_TIMER = 10; //seconds protected const ENQUEUE_TIMER = 60; //seconds protected array $schedules = []; + protected Connections $connections; abstract public static function getName(): string; + abstract public static function getSupportedResource(): string; abstract protected function enqueueResources( - Group $pools, - Database $dbForConsole + array $pools, + callable $getConsoleDB ); public function __construct() { + $this->connections = new Connections(); $type = static::getSupportedResource(); $this ->desc("Execute {$type}s scheduled in Appwrite") ->inject('pools') - ->inject('dbForConsole') + ->inject('getConsoleDB') ->inject('getProjectDB') - ->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB)); + ->callback(fn (array $pools, callable $getConsoleDB, callable $getProjectDB) => $this->action($pools, $getConsoleDB, $getProjectDB)); } /** @@ -47,11 +47,12 @@ abstract class ScheduleBase extends Action * 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute * 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker. */ - public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void + public function action(array $pools, callable $getConsoleDB, callable $getProjectDB): void { Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1'); Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started'); + [$_, $_, $dbForConsole] = $getConsoleDB(); /** * Extract only necessary attributes to lower memory used. * @@ -127,76 +128,74 @@ abstract class ScheduleBase extends Action $latestDocument = \end($results); } - $pools->reclaim(); Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds"); Console::success("Starting timers at " . DateTime::now()); - run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { - /** - * The timer synchronize $schedules copy with database collection. - */ - Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { - $time = DateTime::now(); - $timerStart = \microtime(true); - $limit = 1000; - $sum = $limit; - $total = 0; - $latestDocument = null; + Timer::tick(static::UPDATE_TIMER * 1000, function () use ($getConsoleDB, &$lastSyncUpdate, $getSchedule, $pools) { + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $connections = new Connections(); + $connections->add($connection, $pool); - Console::log("Sync tick: Running at $time"); + $time = DateTime::now(); + $timerStart = \microtime(true); - while ($sum === $limit) { - $paginationQueries = [Query::limit($limit)]; + $limit = 1000; + $sum = $limit; + $total = 0; + $latestDocument = null; - if ($latestDocument) { - $paginationQueries[] = Query::cursorAfter($latestDocument); - } + Console::log("Sync tick: Running at $time"); - $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), - Query::equal('resourceType', [static::getSupportedResource()]), - Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), - ])); + while ($sum === $limit) { + $paginationQueries = [Query::limit($limit)]; - $sum = count($results); - $total = $total + $sum; - - foreach ($results as $document) { - $localDocument = $this->schedules[$document->getInternalId()] ?? null; - - // Check if resource has been updated since last sync - $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; - $new = \strtotime($document['resourceUpdatedAt']); - - if (!$document['active']) { - Console::info("Removing: {$document['resourceId']}"); - unset($this->schedules[$document->getInternalId()]); - } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceId']}"); - $this->schedules[$document->getInternalId()] = $getSchedule($document); - } - } - - $latestDocument = \end($results); + if ($latestDocument) { + $paginationQueries[] = Query::cursorAfter($latestDocument); } - $lastSyncUpdate = $time; - $timerEnd = \microtime(true); + $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ + Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('resourceType', [static::getSupportedResource()]), + Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), + ])); - $pools->reclaim(); + $sum = count($results); + $total = $total + $sum; - Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); - }); + foreach ($results as $document) { + $localDocument = $this->schedules[$document->getInternalId()] ?? null; - Timer::tick( - static::ENQUEUE_TIMER * 1000, - fn () => $this->enqueueResources($pools, $dbForConsole) - ); + // Check if resource has been updated since last sync + $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; + $new = \strtotime($document['resourceUpdatedAt']); - $this->enqueueResources($pools, $dbForConsole); + if (!$document['active']) { + Console::info("Removing: {$document['resourceId']}"); + unset($this->schedules[$document->getInternalId()]); + } elseif ($new !== $org) { + Console::info("Updating: {$document['resourceId']}"); + $this->schedules[$document->getInternalId()] = $getSchedule($document); + } + } + + $latestDocument = \end($results); + } + + $lastSyncUpdate = $time; + $timerEnd = \microtime(true); + + $connections->reclaim(); + Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); }); + + Timer::tick( + static::ENQUEUE_TIMER * 1000, + fn () => $this->enqueueResources($pools, $getConsoleDB) + ); + + $this->enqueueResources($pools, $getConsoleDB); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 682d796585..b67a892b2e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -4,8 +4,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Swoole\Coroutine as Co; -use Utopia\Database\Database; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleExecutions extends ScheduleBase { @@ -22,11 +21,16 @@ class ScheduleExecutions extends ScheduleBase return 'execution'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); - $queueForFunctions = new Func($connection); + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $this->connections->add($connection, $pool); + + $queuePool = $pools['pools-queue-queue']['pool']; + $queueConnection = $queuePool->get(); + $this->connections->add($queueConnection, $queuePool); + + $queueForFunctions = new Func(new Redis($queueConnection)); $intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds'); foreach ($this->schedules as $schedule) { @@ -52,7 +56,7 @@ class ScheduleExecutions extends ScheduleBase $delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp(); - \go(function () use ($queueForFunctions, $schedule, $delay, $data) { + \go(function () use ($queueForFunctions, $schedule, $delay, $data, $dbForConsole) { Co::sleep($delay); $queueForFunctions->setType('schedule') @@ -67,16 +71,16 @@ class ScheduleExecutions extends ScheduleBase ->setProject($schedule['project']) ->setUserId($data['userId'] ?? '') ->trigger(); - }); - $dbForConsole->deleteDocument( - 'schedules', - $schedule['$id'], - ); + $dbForConsole->deleteDocument( + 'schedules', + $schedule['$id'], + ); + }); unset($this->schedules[$schedule['$internalId']]); } - $queue->reclaim(); + $this->connections->reclaim(); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index e2c278714f..450551400e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -5,9 +5,8 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Cron\CronExpression; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\DateTime; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleFunctions extends ScheduleBase { @@ -26,7 +25,7 @@ class ScheduleFunctions extends ScheduleBase return 'function'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { $timerStart = \microtime(true); $time = DateTime::now(); @@ -68,8 +67,11 @@ class ScheduleFunctions extends ScheduleBase \go(function () use ($delay, $scheduleKeys, $pools) { \sleep($delay); // in seconds - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); + $pool = $pools['pools-queue-queue']['pool']; + $connection = $pool->get(); + $this->connections->add($connection, $pool); + + $queueConnection = new Redis($connection); foreach ($scheduleKeys as $scheduleKey) { // Ensure schedule was not deleted @@ -79,7 +81,7 @@ class ScheduleFunctions extends ScheduleBase $schedule = $this->schedules[$scheduleKey]; - $queueForFunctions = new Func($connection); + $queueForFunctions = new Func($queueConnection); $queueForFunctions ->setType('schedule') @@ -90,7 +92,8 @@ class ScheduleFunctions extends ScheduleBase ->trigger(); } - $queue->reclaim(); + $this->connections->reclaim(); + // $queue->reclaim(); // TODO: Do in try/catch/finally, or add to connectons resource }); } diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 167f1282ed..72e1a5f786 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -3,8 +3,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Messaging; -use Utopia\Database\Database; -use Utopia\Pools\Group; +use Utopia\Queue\Connection\Redis; class ScheduleMessages extends ScheduleBase { @@ -21,8 +20,11 @@ class ScheduleMessages extends ScheduleBase return 'message'; } - protected function enqueueResources(Group $pools, Database $dbForConsole): void + protected function enqueueResources(array $pools, callable $getConsoleDB): void { + [$connection,$pool, $dbForConsole] = $getConsoleDB(); + $this->connections->add($connection, $pool); + foreach ($this->schedules as $schedule) { if (!$schedule['active']) { continue; @@ -35,10 +37,15 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $pools, $dbForConsole) { - $queue = $pools->get('queue')->pop(); - $connection = $queue->getResource(); - $queueForMessaging = new Messaging($connection); + \go(function () use ($now, $schedule, $pools, $dbForConsole) { + $pool = $pools['pools-queue-queue']['pool']; + $dsn = $pools['pools-queue-queue']['dsn']; + $connection = $pool->get(); + $this->connections->add($connection, $pool); + + $queueConnection = new Redis($dsn->getHost(), $dsn->getPort()); + + $queueForMessaging = new Messaging($queueConnection); $queueForMessaging ->setType(MESSAGE_SEND_TYPE_EXTERNAL) @@ -51,8 +58,7 @@ class ScheduleMessages extends ScheduleBase $schedule['$id'], ); - $queue->reclaim(); - + $this->connections->reclaim(); unset($this->schedules[$schedule['$internalId']]); }); } diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index e171f2f405..6e69798d98 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -6,21 +6,26 @@ use Appwrite\Specification\Format\OpenAPI3; use Appwrite\Specification\Format\Swagger2; use Appwrite\Specification\Specification; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use Exception; -use Swoole\Http\Response as HttpResponse; -use Utopia\App; +use Swoole\Http\Request as SwooleHttpRequest; +use Swoole\Http\Response as SwooleHttpResponse; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Http\Adapter\FPM\Server; +use Utopia\Http\Adapter\Swoole\Request; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; +use Utopia\Http\Http; +use Utopia\Http\Validator\Text; +use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; -use Utopia\Registry\Registry; -use Utopia\Request; use Utopia\System\System; -use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; class Specs extends Action { @@ -35,21 +40,32 @@ class Specs extends Action ->desc('Generate Appwrite API specifications') ->param('version', 'latest', new Text(16), 'Spec version', true) ->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true) - ->inject('register') - ->callback(fn (string $version, string $mode, Registry $register) => $this->action($version, $mode, $register)); + ->inject('context') + ->callback(fn (string $version, string $mode, Container $context) => $this->action($version, $mode, $context)); } - public function action(string $version, string $mode, Registry $register): void + public function action(string $version, string $mode, Container $container): void { - $appRoutes = App::getRoutes(); - $response = new Response(new HttpResponse()); + $appRoutes = Http::getRoutes(); + $response = new Response(new HttpResponse(new SwooleHttpResponse())); $mocks = ($mode === 'mocks'); + $requestDependency = new Dependency(); + $responseDependency = new Dependency(); + $dbForConsole = new Dependency(); + $dbForProject = new Dependency(); + // Mock dependencies - App::setResource('request', fn () => new Request()); - App::setResource('response', fn () => $response); - App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None()))); - App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); + $requestDependency->setName('request')->setCallback(fn () => new Request(new SwooleHttpRequest())); + $responseDependency->setName('response')->setCallback(fn () => $response); + $dbForConsole->setName('dbForConsole')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); + $dbForProject->setName('dbForProject')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); + + $container + ->set($requestDependency) + ->set($responseDependency) + ->set($dbForProject) + ->set($dbForConsole); $platforms = [ 'client' => APP_PLATFORM_CLIENT, @@ -241,7 +257,7 @@ class Specs extends Action ]; } - $models = $response->getModels(); + $models = Models::getModels(); foreach ($models as $key => $value) { if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { @@ -249,7 +265,7 @@ class Specs extends Action } } - $arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; + $arguments = [new Http(new Server(), $container, 'UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; foreach (['swagger2', 'open-api3'] as $format) { $formatInstance = match ($format) { 'swagger2' => new Swagger2(...$arguments), diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 341ce42fc4..608b924c06 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -3,8 +3,8 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; -use Utopia\Validator\Boolean; -use Utopia\Validator\Text; +use Utopia\Http\Validator\Boolean; +use Utopia\Http\Validator\Text; class Upgrade extends Install { diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 86ca59d3fd..d21f1c267d 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -9,6 +9,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Structure; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\Platform\Action; use Utopia\Queue\Message; @@ -28,7 +29,8 @@ class Audits extends Action ->desc('Audits worker') ->inject('message') ->inject('dbForProject') - ->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject)); + ->inject('authorization') + ->callback(fn ($message, $dbForProject, ValidatorAuthorization $authorization) => $this->action($message, $dbForProject, $authorization)); } @@ -41,7 +43,7 @@ class Audits extends Action * @throws Authorization * @throws Structure */ - public function action(Message $message, Database $dbForProject): void + public function action(Message $message, Database $dbForProject, ValidatorAuthorization $auth): void { $payload = $message->getPayload() ?? []; diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 5dd2f7f886..9274e7430c 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -54,7 +54,8 @@ class Builds extends Action ->inject('dbForProject') ->inject('deviceForFunctions') ->inject('log') - ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); + ->inject('authorization') + ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $authorization) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log, $authorization)); } /** @@ -67,10 +68,11 @@ class Builds extends Action * @param Database $dbForProject * @param Device $deviceForFunctions * @param Log $log + * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void + public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $auth): void { $payload = $message->getPayload() ?? []; @@ -92,7 +94,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log, $auth); break; default: @@ -113,11 +115,12 @@ class Builds extends Action * @param Document $deployment * @param Document $template * @param Log $log + * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log, Authorization $auth): void { $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); @@ -221,20 +224,18 @@ class Builds extends Action $templateRootDirectory = \ltrim($templateRootDirectory, '/'); if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { - $stdout = ''; - $stderr = ''; - + $output = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommandForTemplate, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); $tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz'; @@ -245,7 +246,7 @@ class Builds extends Action } $tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory)); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -254,7 +255,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr); + Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $output); $directorySize = $deviceForFunctions->getFileSize($source); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -276,6 +277,7 @@ class Builds extends Action $branchName = $deployment->getAttribute('providerBranch'); $commitHash = $deployment->getAttribute('providerCommitHash', ''); + $output = ''; $cloneVersion = $branchName; $cloneType = GitHub::CLONE_TYPE_BRANCH; @@ -283,22 +285,18 @@ class Builds extends Action $cloneVersion = $commitHash; $cloneType = GitHub::CLONE_TYPE_COMMIT; } - $gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory); - $stdout = ''; - $stderr = ''; - - Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $output); if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); return; } - $exit = Console::execute($gitCloneCommand, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommand, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Local refactoring for function folder with spaces @@ -306,10 +304,10 @@ class Builds extends Action $rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory); $from = $tmpDirectory . '/' . $rootDirectory; $to = $tmpDirectory . '/' . $rootDirectoryWithoutSpaces; - $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $stdout, $stderr); + $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $output); if ($exit !== 0) { - throw new \Exception('Unable to move function with spaces' . $stderr); + throw new \Exception('Unable to move function with spaces' . $output); } $rootDirectory = $rootDirectoryWithoutSpaces; } @@ -330,33 +328,33 @@ class Builds extends Action $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); + $exit = Console::execute($gitCloneCommandForTemplate, '', $output); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $stderr); + throw new \Exception('Unable to clone code repository: ' . $output); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); - Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); + Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); // Merge template into user repo - Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); + Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); // Commit and push - $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr); + $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $output); if ($exit !== 0) { - throw new \Exception('Unable to push code repository: ' . $stderr); + throw new \Exception('Unable to push code repository: ' . $output); } - $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr); + $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $output); if ($exit !== 0) { - throw new \Exception('Unable to get vcs commit SHA: ' . $stderr); + throw new \Exception('Unable to get vcs commit SHA: ' . $output); } - $providerCommitHash = \trim($stdout); + $providerCommitHash = \trim($output); $authorUrl = "https://github.com/$cloneOwner"; $deployment->setAttribute('providerCommitHash', $providerCommitHash ?? ''); @@ -399,7 +397,7 @@ class Builds extends Action } $tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -408,7 +406,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr); + Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $output); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -663,7 +661,7 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + $auth->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } catch (\Throwable $th) { if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 58dc1dd28a..4a8d928ba2 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -11,7 +11,6 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model\Rule; use Exception; use Throwable; -use Utopia\App; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -22,6 +21,7 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Domains\Domain; +use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -330,30 +330,26 @@ class Certificates extends Action * * @param string $folder Folder into which certificates should be generated * @param string $domain Domain to generate certificate for - * @return array Named array with keys 'stdout' and 'stderr', both string + * @return string output * @throws Exception */ - private function issueCertificate(string $folder, string $domain, string $email): array + private function issueCertificate(string $folder, string $domain, string $email): string { - $stdout = ''; - $stderr = ''; + $output = ''; - $staging = (App::isProduction()) ? '' : ' --dry-run'; + $staging = (Http::isProduction()) ? '' : ' --dry-run'; $exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}" . " --email " . $email . " --cert-name " . $folder . " -w " . APP_STORAGE_CERTIFICATES - . " -d {$domain}", '', $stdout, $stderr); + . " -d {$domain}", '', $output); // Unexpected error, usually 5XX, API limits, ... if ($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: ' . $stderr); + throw new Exception('Failed to issue a certificate with message: ' . $output); } - return [ - 'stdout' => $stdout, - 'stderr' => $stderr - ]; + return $output; } /** @@ -381,7 +377,7 @@ class Certificates extends Action * @return void * @throws Exception */ - private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void + private function applyCertificateFiles(string $folder, string $domain, string $letsEncryptData): void { // Prepare folder in storage for domain @@ -394,19 +390,19 @@ class Certificates extends Action // Move generated files if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData); } $config = \implode(PHP_EOL, [ diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 4f8953a270..478b8bb80d 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -54,14 +55,15 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log)); + ->inject('authorization') + ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $authorization) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log, $authorization)); } /** * @throws Exception * @throws Throwable */ - public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void + public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $auth): void { $payload = $message->getPayload() ?? []; @@ -117,7 +119,7 @@ class Deletes extends Action break; case DELETE_TYPE_AUDIT: if (!$project->isEmpty()) { - $this->deleteAuditLogs($project, $getProjectDB, $auditRetention); + $this->deleteAuditLogs($project, $getProjectDB, $auditRetention, $auth); } if (!$document->isEmpty()) { @@ -125,7 +127,7 @@ class Deletes extends Action } break; case DELETE_TYPE_ABUSE: - $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention); + $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention, $auth); break; case DELETE_TYPE_REALTIME: $this->deleteRealtimeUsage($dbForConsole, $datetime); @@ -693,7 +695,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void + private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention, ValidatorAuthorization $auth): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -714,7 +716,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention): void + private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention, ValidatorAuthorization $auth): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index e48f96ea90..51bdb56cf0 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -84,13 +84,13 @@ class Mails extends Action $bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'; } $bodyTemplate = Template::fromFile($bodyTemplate); - $bodyTemplate->setParam('{{body}}', $body, escapeHtml: false); + $bodyTemplate->setParam('{{body}}', $body, escape: false); foreach ($variables as $key => $value) { // TODO: hotfix for redirect param - $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect'); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: $key !== 'redirect'); } foreach ($this->richTextParams as $key => $value) { - $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: false); } $body = $bodyTemplate->render(); diff --git a/src/Appwrite/Promises/Promise.php b/src/Appwrite/Promises/Promise.php index a6b1aa79d5..f12590dfed 100644 --- a/src/Appwrite/Promises/Promise.php +++ b/src/Appwrite/Promises/Promise.php @@ -12,7 +12,7 @@ abstract class Promise private mixed $result; - public function __construct(?callable $executor = null) + final public function __construct(?callable $executor = null) { if (\is_null($executor)) { return; diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php index c258ef6a5e..8cddd567f3 100644 --- a/src/Appwrite/Promises/Swoole.php +++ b/src/Appwrite/Promises/Swoole.php @@ -6,23 +6,16 @@ use Swoole\Coroutine\Channel; class Swoole extends Promise { - public function __construct(?callable $executor = null) - { - parent::__construct($executor); - } - protected function execute( callable $executor, callable $resolve, callable $reject ): void { - \go(function () use ($executor, $resolve, $reject) { - try { - $executor($resolve, $reject); - } catch (\Throwable $exception) { - $reject($exception); - } - }); + try { + $executor($resolve, $reject); + } catch (\Throwable $exception) { + $reject($exception); + } } /** diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index 30ce6470e1..e2048971be 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -3,13 +3,13 @@ namespace Appwrite\Specification; use Appwrite\Utopia\Response\Model; -use Utopia\App; use Utopia\Config\Config; -use Utopia\Route; +use Utopia\Http\Http; +use Utopia\Http\Route; abstract class Format { - protected App $app; + protected Http $http; /** * @var Route[] @@ -50,9 +50,9 @@ abstract class Format ] ]; - public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount) + public function __construct(Http $http, array $services, array $routes, array $models, array $keys, int $authCount) { - $this->app = $app; + $this->http = $http; $this->services = $services; $this->routes = $routes; $this->models = $models; diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index 3074d59b7c..c9186ee0ac 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -7,11 +7,11 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Validator; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; -use Utopia\Validator\WhiteList; +use Utopia\Http\Validator; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; +use Utopia\Http\Validator\WhiteList; class OpenAPI3 extends Format { @@ -238,8 +238,11 @@ class OpenAPI3 extends Format } if ($route->getLabel('sdk.response.code', 500) === 204) { - $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['description'] = 'No content'; - unset($temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['schema']); + $labelCode = (string)$route->getLabel('sdk.response.code', '500'); + $temp['responses'][$labelCode]['description'] = 'No content'; + if (isset($temp['responses'][$labelCode]['schema'])) { + unset($temp['responses'][$labelCode]['schema']); + } } if ((!empty($scope))) { // && 'public' != $scope @@ -269,10 +272,10 @@ class OpenAPI3 extends Format $bodyRequired = []; foreach ($route->getParams() as $name => $param) { // Set params - /** - * @var \Utopia\Validator $validator - */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; + $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); + + /** @var Validator $validator */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; $node = [ 'name' => $name, @@ -289,11 +292,11 @@ class OpenAPI3 extends Format switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = false; break; @@ -314,15 +317,15 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'email'; $node['schema']['x-example'] = 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\URL': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\JSON': + case 'Utopia\Http\Validator\Mock': + case 'Utopia\Http\Validator\Assoc': $param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['schema']['type'] = 'object'; $node['schema']['x-example'] = '{}'; @@ -332,7 +335,7 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'binary'; break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': /** @var ArrayList $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -394,25 +397,25 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'phone'; $node['schema']['x-example'] = '+12065550100'; // In the US, 555 is reserved like example.com break; - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Range': /** @var Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = $validator->getMin(); break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Integer': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'int32'; break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $node['schema']['type'] = 'number'; $node['schema']['format'] = 'float'; break; - case 'Utopia\Validator\Length': + case 'Utopia\Http\Validator\Length': $node['schema']['type'] = $validator->getType(); break; - case 'Utopia\Validator\WhiteList': + case 'Utopia\Http\Validator\WhiteList': /** @var WhiteList $validator */ $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 2eab7807b3..81e0f2c41d 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -7,10 +7,10 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Validator; -use Utopia\Validator\ArrayList; -use Utopia\Validator\Nullable; -use Utopia\Validator\Range; +use Utopia\Http\Validator; +use Utopia\Http\Validator\ArrayList; +use Utopia\Http\Validator\Nullable; +use Utopia\Http\Validator\Range; class Swagger2 extends Format { @@ -270,8 +270,10 @@ class Swagger2 extends Format ); foreach ($parameters as $name => $param) { // Set params + $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); + /** @var Validator $validator */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; $node = [ 'name' => $name, @@ -287,18 +289,18 @@ class Swagger2 extends Format } $validatorClass = (!empty($validator)) ? \get_class($validator) : ''; - if ($validatorClass === 'Utopia\Validator\AnyOf') { + if ($validatorClass === 'Utopia\Http\Validator\AnyOf') { $validator = $param['validator']->getValidators()[0]; $validatorClass = \get_class($validator); } switch ($validatorClass) { - case 'Utopia\Validator\Text': + case 'Utopia\Http\Validator\Text': case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case 'Utopia\Http\Validator\Boolean': $node['type'] = $validator->getType(); $node['x-example'] = false; break; @@ -319,13 +321,13 @@ class Swagger2 extends Format $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': + case 'Utopia\Http\Validator\Host': + case 'Utopia\Http\Validator\URL': $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\ArrayList': + case 'Utopia\Http\Validator\ArrayList': /** @var ArrayList $validator */ $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; @@ -333,9 +335,9 @@ class Swagger2 extends Format 'type' => $validator->getValidator()->getType(), ]; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case 'Utopia\Http\Validator\JSON': + case 'Utopia\Http\Validator\Mock': + case 'Utopia\Http\Validator\Assoc': $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = '{}'; @@ -406,26 +408,26 @@ class Swagger2 extends Format $node['format'] = 'phone'; $node['x-example'] = '+12065550100'; break; - case 'Utopia\Validator\Range': + case 'Utopia\Http\Validator\Range': /** @var Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = $validator->getMin(); break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Integer': + case 'Utopia\Http\Validator\Numeric': + case 'Utopia\Http\Validator\Integer': $node['type'] = $validator->getType(); $node['format'] = 'int32'; break; - case 'Utopia\Validator\FloatValidator': + case 'Utopia\Http\Validator\FloatValidator': $node['type'] = 'number'; $node['format'] = 'float'; break; - case 'Utopia\Validator\Length': + case 'Utopia\Http\Validator\Length': $node['type'] = $validator->getType(); break; - case 'Utopia\Validator\WhiteList': - /** @var \Utopia\Validator\WhiteList $validator */ + case 'Utopia\Http\Validator\WhiteList': + /** @var \Utopia\Http\Validator\WhiteList $validator */ $node['type'] = $validator->getType(); $node['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Task/Validator/Cron.php b/src/Appwrite/Task/Validator/Cron.php index 03bd1c5220..afa19c50a6 100644 --- a/src/Appwrite/Task/Validator/Cron.php +++ b/src/Appwrite/Task/Validator/Cron.php @@ -3,7 +3,7 @@ namespace Appwrite\Task\Validator; use Cron\CronExpression; -use Utopia\Validator; +use Utopia\Http\Validator; class Cron extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php index 3f23500952..b851d8ba85 100644 --- a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php +++ b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php @@ -3,7 +3,7 @@ namespace Appwrite\Utopia\Database\Validator; use Utopia\Database\Validator\UID; -use Utopia\Validator; +use Utopia\Http\Validator; class CompoundUID extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/ProjectId.php b/src/Appwrite/Utopia/Database/Validator/ProjectId.php index 46b0cdf53e..28abe176fe 100644 --- a/src/Appwrite/Utopia/Database/Validator/ProjectId.php +++ b/src/Appwrite/Utopia/Database/Validator/ProjectId.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia\Database\Validator; -use Utopia\Validator; +use Utopia\Http\Validator; class ProjectId extends Validator { diff --git a/src/Appwrite/Utopia/Queue/Connections.php b/src/Appwrite/Utopia/Queue/Connections.php new file mode 100644 index 0000000000..e873373566 --- /dev/null +++ b/src/Appwrite/Utopia/Queue/Connections.php @@ -0,0 +1,36 @@ +connections[] = ['connection' => $connection, 'pool' => $pool]; + return $this; + } + + /** + * @return self + */ + public function reclaim(): self + { + foreach ($this->connections as $id => $resource) { + $pool = $resource['pool']; + $connection = $resource['connection']; + $pool->put($connection); + unset($this->connections[$id]); + } + + return $this; + } +} diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 3f0a196d5e..6be9701baa 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -3,11 +3,10 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Request\Filter; -use Swoole\Http\Request as SwooleRequest; -use Utopia\Route; -use Utopia\Swoole\Request as UtopiaRequest; +use Utopia\Http\Adapter\Swoole\Request as HttpRequest; +use Utopia\Http\Route; -class Request extends UtopiaRequest +class Request extends HttpRequest { /** * @var array @@ -15,9 +14,12 @@ class Request extends UtopiaRequest private array $filters = []; private static ?Route $route = null; - public function __construct(SwooleRequest $request) + /** + * Request constructor. + */ + public function __construct(HttpRequest $request) { - parent::__construct($request); + parent::__construct($request->swoole); } /** @@ -113,6 +115,16 @@ class Request extends UtopiaRequest return self::$route !== null; } + + public function removeHeader(string $key): static + { + if (isset($this->headers[$key])) { + unset($this->headers[$key]); + } + + return parent::removeHeader($key); + } + /** * Get headers * @@ -122,10 +134,14 @@ class Request extends UtopiaRequest */ public function getHeaders(): array { - $headers = $this->generateHeaders(); + if ($this->headers !== null) { + return $this->headers; + } + + $this->headers = $this->generateHeaders(); if (empty($this->swoole->cookie)) { - return $headers; + return $this->headers; } $cookieHeaders = []; @@ -134,10 +150,10 @@ class Request extends UtopiaRequest } if (!empty($cookieHeaders)) { - $headers['cookie'] = \implode('; ', $cookieHeaders); + $this->headers['cookie'] = \implode('; ', $cookieHeaders); } - return $headers; + return $this->headers; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 6cc2639f51..376c327b37 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -4,122 +4,20 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; -use Appwrite\Utopia\Response\Model; -use Appwrite\Utopia\Response\Model\Account; -use Appwrite\Utopia\Response\Model\AlgoArgon2; -use Appwrite\Utopia\Response\Model\AlgoBcrypt; -use Appwrite\Utopia\Response\Model\AlgoMd5; -use Appwrite\Utopia\Response\Model\AlgoPhpass; -use Appwrite\Utopia\Response\Model\AlgoScrypt; -use Appwrite\Utopia\Response\Model\AlgoScryptModified; -use Appwrite\Utopia\Response\Model\AlgoSha; -use Appwrite\Utopia\Response\Model\Any; -use Appwrite\Utopia\Response\Model\Attribute; -use Appwrite\Utopia\Response\Model\AttributeBoolean; -use Appwrite\Utopia\Response\Model\AttributeDatetime; -use Appwrite\Utopia\Response\Model\AttributeEmail; -use Appwrite\Utopia\Response\Model\AttributeEnum; -use Appwrite\Utopia\Response\Model\AttributeFloat; -use Appwrite\Utopia\Response\Model\AttributeInteger; -use Appwrite\Utopia\Response\Model\AttributeIP; -use Appwrite\Utopia\Response\Model\AttributeList; -use Appwrite\Utopia\Response\Model\AttributeRelationship; -use Appwrite\Utopia\Response\Model\AttributeString; -use Appwrite\Utopia\Response\Model\AttributeURL; -use Appwrite\Utopia\Response\Model\AuthProvider; -use Appwrite\Utopia\Response\Model\BaseList; -use Appwrite\Utopia\Response\Model\Branch; -use Appwrite\Utopia\Response\Model\Bucket; -use Appwrite\Utopia\Response\Model\Build; -use Appwrite\Utopia\Response\Model\Collection; -use Appwrite\Utopia\Response\Model\ConsoleVariables; -use Appwrite\Utopia\Response\Model\Continent; -use Appwrite\Utopia\Response\Model\Country; -use Appwrite\Utopia\Response\Model\Currency; -use Appwrite\Utopia\Response\Model\Database; -use Appwrite\Utopia\Response\Model\Deployment; -use Appwrite\Utopia\Response\Model\Detection; -use Appwrite\Utopia\Response\Model\Document as ModelDocument; -use Appwrite\Utopia\Response\Model\Error; -use Appwrite\Utopia\Response\Model\ErrorDev; -use Appwrite\Utopia\Response\Model\Execution; -use Appwrite\Utopia\Response\Model\File; -use Appwrite\Utopia\Response\Model\Func; -use Appwrite\Utopia\Response\Model\Headers; -use Appwrite\Utopia\Response\Model\HealthAntivirus; -use Appwrite\Utopia\Response\Model\HealthCertificate; -use Appwrite\Utopia\Response\Model\HealthQueue; -use Appwrite\Utopia\Response\Model\HealthStatus; -use Appwrite\Utopia\Response\Model\HealthTime; -use Appwrite\Utopia\Response\Model\HealthVersion; -use Appwrite\Utopia\Response\Model\Identity; -use Appwrite\Utopia\Response\Model\Index; -use Appwrite\Utopia\Response\Model\Installation; -use Appwrite\Utopia\Response\Model\JWT; -use Appwrite\Utopia\Response\Model\Key; -use Appwrite\Utopia\Response\Model\Language; -use Appwrite\Utopia\Response\Model\Locale; -use Appwrite\Utopia\Response\Model\LocaleCode; -use Appwrite\Utopia\Response\Model\Log; -use Appwrite\Utopia\Response\Model\Membership; -use Appwrite\Utopia\Response\Model\Message; -use Appwrite\Utopia\Response\Model\Metric; -use Appwrite\Utopia\Response\Model\MetricBreakdown; -use Appwrite\Utopia\Response\Model\MFAChallenge; -use Appwrite\Utopia\Response\Model\MFAFactors; -use Appwrite\Utopia\Response\Model\MFARecoveryCodes; -use Appwrite\Utopia\Response\Model\MFAType; -use Appwrite\Utopia\Response\Model\Migration; -use Appwrite\Utopia\Response\Model\MigrationFirebaseProject; -use Appwrite\Utopia\Response\Model\MigrationReport; -use Appwrite\Utopia\Response\Model\Mock; -use Appwrite\Utopia\Response\Model\MockNumber; -use Appwrite\Utopia\Response\Model\None; -use Appwrite\Utopia\Response\Model\Phone; -use Appwrite\Utopia\Response\Model\Platform; -use Appwrite\Utopia\Response\Model\Preferences; -use Appwrite\Utopia\Response\Model\Project; -use Appwrite\Utopia\Response\Model\Provider; -use Appwrite\Utopia\Response\Model\ProviderRepository; -use Appwrite\Utopia\Response\Model\Rule; -use Appwrite\Utopia\Response\Model\Runtime; -use Appwrite\Utopia\Response\Model\Session; -use Appwrite\Utopia\Response\Model\Specification; -use Appwrite\Utopia\Response\Model\Subscriber; -use Appwrite\Utopia\Response\Model\Target; -use Appwrite\Utopia\Response\Model\Team; -use Appwrite\Utopia\Response\Model\TemplateEmail; -use Appwrite\Utopia\Response\Model\TemplateFunction; -use Appwrite\Utopia\Response\Model\TemplateRuntime; -use Appwrite\Utopia\Response\Model\TemplateSMS; -use Appwrite\Utopia\Response\Model\TemplateVariable; -use Appwrite\Utopia\Response\Model\Token; -use Appwrite\Utopia\Response\Model\Topic; -use Appwrite\Utopia\Response\Model\UsageBuckets; -use Appwrite\Utopia\Response\Model\UsageCollection; -use Appwrite\Utopia\Response\Model\UsageDatabase; -use Appwrite\Utopia\Response\Model\UsageDatabases; -use Appwrite\Utopia\Response\Model\UsageFunction; -use Appwrite\Utopia\Response\Model\UsageFunctions; -use Appwrite\Utopia\Response\Model\UsageProject; -use Appwrite\Utopia\Response\Model\UsageStorage; -use Appwrite\Utopia\Response\Model\UsageUsers; -use Appwrite\Utopia\Response\Model\User; -use Appwrite\Utopia\Response\Model\Variable; -use Appwrite\Utopia\Response\Model\VcsContent; -use Appwrite\Utopia\Response\Model\Webhook; +use Appwrite\Utopia\Response\Models; use Exception; use JsonException; -use Swoole\Http\Response as SwooleHTTPResponse; // Keep last use Utopia\Database\Document; -use Utopia\Swoole\Response as SwooleResponse; +use Utopia\Http\Adapter\Swoole\Response as HttpResponse; + +// Keep last /** * @method int getStatusCode() * @method Response setStatusCode(int $code = 200) */ -class Response extends SwooleResponse +class Response extends HttpResponse { // General public const MODEL_NONE = 'none'; @@ -330,162 +228,9 @@ class Response extends SwooleResponse * * @param float $time */ - public function __construct(SwooleHTTPResponse $response) + public function __construct(HttpResponse $response) { - $this - // General - ->setModel(new None()) - ->setModel(new Any()) - ->setModel(new Error()) - ->setModel(new ErrorDev()) - // Lists - ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) - ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) - ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) - ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) - ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) - ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) - ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) - ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) - ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) - ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) - ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) - ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) - ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) - ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) - ->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY)) - ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) - ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) - ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) - ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) - ->setModel(new BaseList('Builds List', self::MODEL_BUILD_LIST, 'builds', self::MODEL_BUILD)) // Not used anywhere yet - ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) - ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) - ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) - ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) - ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) - ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) - ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) - ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) - ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) - ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) - ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) - ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) - ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) - ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) - ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) - ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) - ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) - ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) - ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) - ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) - ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) - ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) - // Entities - ->setModel(new Database()) - ->setModel(new Collection()) - ->setModel(new Attribute()) - ->setModel(new AttributeList()) - ->setModel(new AttributeString()) - ->setModel(new AttributeInteger()) - ->setModel(new AttributeFloat()) - ->setModel(new AttributeBoolean()) - ->setModel(new AttributeEmail()) - ->setModel(new AttributeEnum()) - ->setModel(new AttributeIP()) - ->setModel(new AttributeURL()) - ->setModel(new AttributeDatetime()) - ->setModel(new AttributeRelationship()) - ->setModel(new Index()) - ->setModel(new ModelDocument()) - ->setModel(new Log()) - ->setModel(new User()) - ->setModel(new AlgoMd5()) - ->setModel(new AlgoSha()) - ->setModel(new AlgoPhpass()) - ->setModel(new AlgoBcrypt()) - ->setModel(new AlgoScrypt()) - ->setModel(new AlgoScryptModified()) - ->setModel(new AlgoArgon2()) - ->setModel(new Account()) - ->setModel(new Preferences()) - ->setModel(new Session()) - ->setModel(new Identity()) - ->setModel(new Token()) - ->setModel(new JWT()) - ->setModel(new Locale()) - ->setModel(new LocaleCode()) - ->setModel(new File()) - ->setModel(new Bucket()) - ->setModel(new Team()) - ->setModel(new Membership()) - ->setModel(new Func()) - ->setModel(new TemplateFunction()) - ->setModel(new TemplateRuntime()) - ->setModel(new TemplateVariable()) - ->setModel(new Installation()) - ->setModel(new ProviderRepository()) - ->setModel(new Detection()) - ->setModel(new VcsContent()) - ->setModel(new Branch()) - ->setModel(new Runtime()) - ->setModel(new Deployment()) - ->setModel(new Execution()) - ->setModel(new Build()) - ->setModel(new Project()) - ->setModel(new Webhook()) - ->setModel(new Key()) - ->setModel(new MockNumber()) - ->setModel(new AuthProvider()) - ->setModel(new Platform()) - ->setModel(new Variable()) - ->setModel(new Country()) - ->setModel(new Continent()) - ->setModel(new Language()) - ->setModel(new Currency()) - ->setModel(new Phone()) - ->setModel(new HealthAntivirus()) - ->setModel(new HealthQueue()) - ->setModel(new HealthStatus()) - ->setModel(new HealthCertificate()) - ->setModel(new HealthTime()) - ->setModel(new HealthVersion()) - ->setModel(new Metric()) - ->setModel(new MetricBreakdown()) - ->setModel(new UsageDatabases()) - ->setModel(new UsageDatabase()) - ->setModel(new UsageCollection()) - ->setModel(new UsageUsers()) - ->setModel(new UsageStorage()) - ->setModel(new UsageBuckets()) - ->setModel(new UsageFunctions()) - ->setModel(new UsageFunction()) - ->setModel(new UsageProject()) - ->setModel(new Headers()) - ->setModel(new Specification()) - ->setModel(new Rule()) - ->setModel(new TemplateSMS()) - ->setModel(new TemplateEmail()) - ->setModel(new ConsoleVariables()) - ->setModel(new MFAChallenge()) - ->setModel(new MFARecoveryCodes()) - ->setModel(new MFAType()) - ->setModel(new MFAFactors()) - ->setModel(new Provider()) - ->setModel(new Message()) - ->setModel(new Topic()) - ->setModel(new Subscriber()) - ->setModel(new Target()) - ->setModel(new Migration()) - ->setModel(new MigrationReport()) - ->setModel(new MigrationFirebaseProject()) - // Tests (keep last) - ->setModel(new Mock()); - - parent::__construct($response); + parent::__construct($response->swoole); } /** @@ -495,49 +240,6 @@ class Response extends SwooleResponse public const CONTENT_TYPE_NULL = 'null'; public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; - /** - * List of defined output objects - */ - protected $models = []; - - /** - * Set Model Object - * - * @return self - */ - public function setModel(Model $instance) - { - $this->models[$instance->getType()] = $instance; - - return $this; - } - - /** - * Get Model Object - * - * @param string $key - * @return Model - * @throws Exception - */ - public function getModel(string $key): Model - { - if (!isset($this->models[$key])) { - throw new Exception('Undefined model: ' . $key); - } - - return $this->models[$key]; - } - - /** - * Get Models List - * - * @return Model[] - */ - public function getModels(): array - { - return $this->models; - } - public function applyFilters(array $data, string $model): array { foreach ($this->filters as $filter) { @@ -605,7 +307,7 @@ class Response extends SwooleResponse public function output(Document $document, string $model): array { $data = clone $document; - $model = $this->getModel($model); + $model = Models::getModel($model); $output = []; $data = $model->filter($data); @@ -640,7 +342,7 @@ class Response extends SwooleResponse if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { $condition = false; - foreach ($this->getModel($type)->conditions as $attribute => $val) { + foreach (Models::getModel($type)->conditions as $attribute => $val) { $condition = $item->getAttribute($attribute) === $val; if (!$condition) { break; @@ -655,7 +357,7 @@ class Response extends SwooleResponse $ruleType = $rule['type']; } - if (!array_key_exists($ruleType, $this->models)) { + if (!array_key_exists($ruleType, Models::getModels())) { throw new Exception('Missing model for rule: ' . $ruleType); } diff --git a/src/Appwrite/Utopia/Response/Models.php b/src/Appwrite/Utopia/Response/Models.php new file mode 100644 index 0000000000..2a0393321c --- /dev/null +++ b/src/Appwrite/Utopia/Response/Models.php @@ -0,0 +1,304 @@ +getType()] = $instance; + } + + /** + * Get Model Object + * + * @param string $key + * @return Model + * @throws Exception + */ + public static function getModel(string $key): Model + { + if (!isset(self::$models[$key])) { + throw new Exception('Undefined model: ' . $key); + } + + return self::$models[$key]; + } + + public static function getModels(): array + { + return self::$models; + } +} diff --git a/src/Appwrite/Utopia/View.php b/src/Appwrite/Utopia/View.php index e4ed8164c9..60cafb1ca8 100644 --- a/src/Appwrite/Utopia/View.php +++ b/src/Appwrite/Utopia/View.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia; -use Utopia\View as OldView; +use Utopia\View\View as OldView; class View extends OldView { diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 04f2dbd8c8..28c3227979 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -12,7 +12,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Validator\JSON; +use Utopia\Http\Validator\JSON; trait DatabasesBase { @@ -2869,7 +2869,7 @@ trait DatabasesBase $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']); $this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']); $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']); - $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and '.\number_format(PHP_INT_MAX), $tooLow['body']['message']); } /** diff --git a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php index 8484996058..9b7be063db 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php @@ -64,9 +64,10 @@ class DatabasesCustomClientTest extends Scope 'required' => true, ]); + $this->assertEquals(202, $response['headers']['status-code']); + sleep(1); - $this->assertEquals(202, $response['headers']['status-code']); // Document aliases write to update, delete $document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ @@ -82,6 +83,7 @@ class DatabasesCustomClientTest extends Scope ] ]); + $this->assertEquals(201, $document1['headers']['status-code']); $this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php index ca8753f374..4288b92613 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php @@ -17,6 +17,14 @@ class DatabasesPermissionsGuestTest extends Scope use SideClient; use DatabasesPermissionsScope; + protected Authorization $auth; + + public function setUp(): void + { + parent::setUp(); + $this->auth = new Authorization(); + } + public function createCollection(): array { $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -111,8 +119,8 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(201, $publicResponse['headers']['status-code']); $this->assertEquals(201, $privateResponse['headers']['status-code']); - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->auth->getRoles(); + $this->auth->cleanRoles(); $publicDocuments = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -134,7 +142,7 @@ class DatabasesPermissionsGuestTest extends Scope } foreach ($roles as $role) { - Authorization::setRole($role); + $this->auth->addRole($role); } } @@ -145,8 +153,8 @@ class DatabasesPermissionsGuestTest extends Scope $privateCollectionId = $data['privateCollectionId']; $databaseId = $data['databaseId']; - $roles = Authorization::getRoles(); - Authorization::cleanRoles(); + $roles = $this->auth->getRoles(); + $this->auth->cleanRoles(); $publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -222,7 +230,7 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(401, $privateDocument['headers']['status-code']); foreach ($roles as $role) { - Authorization::setRole($role); + $this->auth->addRole($role); } } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 2d94b9f0e3..f9898757f7 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -7,12 +7,11 @@ use Utopia\CLI\Console; trait FunctionsBase { - protected string $stdout = ''; - protected string $stderr = ''; + protected string $output = ''; protected function packageCode($folder) { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); } protected function awaitDeploymentIsBuilt($functionId, $deploymentId, $checkForSuccess = true): void diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2958e6cb5f..dccbf2e544 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -10,12 +10,12 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\App; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class FunctionsCustomServerTest extends Scope { @@ -476,7 +476,6 @@ class FunctionsCustomServerTest extends Scope * and ensure variable works as expected in execution. */ $this->assertEmpty($template['body']['variables']); - // Create function using settings from template. // Deployment is automatically created from template inside endpoint $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ @@ -504,7 +503,6 @@ class FunctionsCustomServerTest extends Scope $this->assertNotEmpty($function['body']['$id']); $functionId = $function['body']['$id']; - // List deployments so we can await deployment build $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', [ 'content-type' => 'application/json', @@ -2464,7 +2462,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($cookie, $response['body']); // Await Aggregation - sleep(App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); + sleep(System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30)); $tries = 0; while (true) { diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 735e4eced5..b77f006bf8 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2498,6 +2498,6 @@ trait Base protected function packageCode($folder): void { - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout); } } diff --git a/tests/e2e/Services/GraphQL/StorageClientTest.php b/tests/e2e/Services/GraphQL/StorageClientTest.php index 9896598c2d..7d6d617c87 100644 --- a/tests/e2e/Services/GraphQL/StorageClientTest.php +++ b/tests/e2e/Services/GraphQL/StorageClientTest.php @@ -183,7 +183,6 @@ class StorageClientTest extends Scope /** * @depends testCreateFile * @param $file - * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 7fea895b1c..6be103629e 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -232,7 +232,6 @@ class StorageServerTest extends Scope /** * @depends testCreateFile * @param $file - * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c41d861f1d..9fc862e85b 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1691,7 +1691,7 @@ class ProjectsConsoleClientTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items', $response['body']['message']); /** * Test for success diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index c3372b98c5..86aaee2ab0 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1272,11 +1272,10 @@ class RealtimeCustomClientTest extends Scope $this->assertNotEmpty($function['body']['$id']); $folder = 'timeout'; - $stderr = ''; - $stdout = ''; + $output = ''; $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', diff --git a/tests/e2e/Services/Storage/StorageCustomClientTest.php b/tests/e2e/Services/Storage/StorageCustomClientTest.php index c723fba50a..55340ab849 100644 --- a/tests/e2e/Services/Storage/StorageCustomClientTest.php +++ b/tests/e2e/Services/Storage/StorageCustomClientTest.php @@ -1089,7 +1089,7 @@ class StorageCustomClientTest extends Scope $this->assertEquals(200, $file['headers']['status-code']); - // Team 1 view success + // Team 2 view success $file = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1112,6 +1112,8 @@ class StorageCustomClientTest extends Scope 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'), ]); + $this->assertEquals($file['headers']['status-code'], 401); + // Team 2 create failure $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', [ 'content-type' => 'multipart/form-data', diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index d2f132e960..54d55e5e68 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -486,11 +486,10 @@ class WebhooksCustomServerTest extends Scope /** * Test for SUCCESS */ - $stderr = ''; - $stdout = ''; + $output = ''; $folder = 'timeout'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index a34b4fcf88..8f86fb8374 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -324,7 +324,7 @@ services: - MYSQL_USER=user - MYSQL_PASSWORD=password - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync' + command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' maildev: image: djfarrelly/maildev diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index 705da42879..1ff5850402 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Auth; use Appwrite\Auth\Auth; +use Appwrite\Auth\Authentication; use PHPUnit\Framework\TestCase; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -13,21 +14,31 @@ use Utopia\Database\Validator\Roles; class AuthTest extends TestCase { + protected Authorization $auth; + protected Authentication $authentication; + + public function setUp(): void + { + parent::setUp(); + $this->auth = new Authorization(); + $this->authentication = new Authentication(); + } + /** * Reset Roles */ public function tearDown(): void { - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); + $this->auth->cleanRoles(); + $this->auth->addRole(Role::any()->toString()); } public function testCookieName(): void { $name = 'cookie-name'; - $this->assertEquals(Auth::setCookieName($name), $name); - $this->assertEquals(Auth::$cookieName, $name); + $this->assertEquals($this->authentication->setCookieName($name), $name); + $this->assertEquals($this->authentication->getCookieName(), $name); } public function testEncodeDecodeSession(): void @@ -347,7 +358,7 @@ class AuthTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(1, $roles); $this->assertContains(Role::guests()->toString(), $roles); } @@ -383,7 +394,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(13, $roles); $this->assertContains(Role::users()->toString(), $roles); @@ -404,21 +415,21 @@ class AuthTest extends TestCase $user['emailVerification'] = false; $user['phoneVerification'] = false; - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles); // Enable single verification type $user['emailVerification'] = true; - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); } public function testPrivilegedUserRoles(): void { - Authorization::setRole(Auth::USER_ROLE_OWNER); + $this->auth->addRole(Auth::USER_ROLE_OWNER); $user = new Document([ '$id' => ID::custom('123'), 'emailVerification' => true, @@ -444,7 +455,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); @@ -462,7 +473,7 @@ class AuthTest extends TestCase public function testAppUserRoles(): void { - Authorization::setRole(Auth::USER_ROLE_APPS); + $this->auth->addRole(Auth::USER_ROLE_APPS); $user = new Document([ '$id' => ID::custom('123'), 'memberships' => [ @@ -486,7 +497,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index dd9833378f..38534b9b16 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -66,9 +66,13 @@ class EventTest extends TestCase $this->assertEquals('eventValue1', $this->object->getParam('eventKey1')); $this->assertEquals('eventValue2', $this->object->getParam('eventKey2')); $this->assertEquals(null, $this->object->getParam('eventKey3')); - global $register; - $pools = $register->get('pools'); - $client = new Client($this->object->getQueue(), $pools->get('queue')->pop()->getResource()); + + global $registry; + $pools = $registry->get('pools'); + $dsn = $pools['pools-queue-queue']['dsn']; + $queue = new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()); + + $client = new Client($this->object->getQueue(), $queue); $this->assertEquals($client->getQueueSize(), 1); } diff --git a/tests/unit/GraphQL/BuilderTest.php b/tests/unit/GraphQL/BuilderTest.php index d79a104c90..f11045f318 100644 --- a/tests/unit/GraphQL/BuilderTest.php +++ b/tests/unit/GraphQL/BuilderTest.php @@ -4,8 +4,10 @@ namespace Tests\Unit\GraphQL; use Appwrite\GraphQL\Types\Mapper; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class BuilderTest extends TestCase { @@ -13,8 +15,9 @@ class BuilderTest extends TestCase public function setUp(): void { - $this->response = new Response(new SwooleResponse()); - Mapper::init($this->response->getModels()); + Models::init(); + $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); + Mapper::init(Models::getModels()); } /** @@ -22,7 +25,7 @@ class BuilderTest extends TestCase */ public function testCreateTypeMapping() { - $model = $this->response->getModel(Response::MODEL_COLLECTION); + $model = Models::getModel(Response::MODEL_COLLECTION); $type = Mapper::model(\ucfirst($model->getType())); $this->assertEquals('Collection', $type->name); } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 8ba0374093..77b3cee7d6 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; class MessagingChannelsTest extends TestCase { @@ -34,8 +35,12 @@ class MessagingChannelsTest extends TestCase 'functions.1', ]; + protected ValidatorAuthorization $auth; + public function setUp(): void { + $this->auth = new ValidatorAuthorization(); + /** * Setup global Counts */ @@ -66,7 +71,7 @@ class MessagingChannelsTest extends TestCase ] ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -90,7 +95,7 @@ class MessagingChannelsTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user); + $roles = Auth::getRoles($user, $this->auth); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php index 536278d55b..8043ec9f94 100644 --- a/tests/unit/Migration/MigrationTest.php +++ b/tests/unit/Migration/MigrationTest.php @@ -36,7 +36,7 @@ abstract class MigrationTest extends TestCase */ public function testMigrationVersions(): void { - require_once __DIR__ . '/../../../app/init.php'; + //require_once __DIR__ . '/../../../app/init.php'; foreach (Migration::$versions as $class) { $this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class)); diff --git a/tests/unit/Utopia/RequestTest.php b/tests/unit/Utopia/RequestTest.php index 73daaa88bc..6124a7a0c1 100644 --- a/tests/unit/Utopia/RequestTest.php +++ b/tests/unit/Utopia/RequestTest.php @@ -7,7 +7,8 @@ use PHPUnit\Framework\TestCase; use Swoole\Http\Request as SwooleRequest; use Tests\Unit\Utopia\Request\Filters\First; use Tests\Unit\Utopia\Request\Filters\Second; -use Utopia\Route; +use Utopia\Http\Adapter\Swoole\Request as UtopiaSwooleRequest; +use Utopia\Http\Route; class RequestTest extends TestCase { @@ -15,7 +16,7 @@ class RequestTest extends TestCase public function setUp(): void { - $this->request = new Request(new SwooleRequest()); + $this->request = new Request(new UtopiaSwooleRequest(new SwooleRequest())); } public function testFilters(): void @@ -36,7 +37,7 @@ class RequestTest extends TestCase // set test header to prevent header populaten inside the request class $this->request->addHeader('EXAMPLE', 'VALUE'); $this->request->setRoute($route); - $this->request->setQueryString([ + $this->request->setQuery([ 'initial' => true, 'first' => false ]); diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 452119fafb..67253e48a0 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -3,12 +3,14 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Models; use Exception; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; use Utopia\Database\Document; +use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class ResponseTest extends TestCase { @@ -16,10 +18,10 @@ class ResponseTest extends TestCase public function setUp(): void { - $this->response = new Response(new SwooleResponse()); - $this->response->setModel(new Single()); - $this->response->setModel(new Lists()); - $this->response->setModel(new Nested()); + $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); + Models::setModel(new Single()); + Models::setModel(new Lists()); + Models::setModel(new Nested()); } public function testFilters(): void From 579d3d7f14fbc23ed1be09bf89500b3313479a1f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:39:43 -0400 Subject: [PATCH 209/241] chore: lint & updates --- app/cli.php | 12 +-- app/init/constants.php | 7 ++ composer.lock | 94 ++++++++++---------- src/Appwrite/Platform/Tasks/ScheduleBase.php | 2 +- 4 files changed, 61 insertions(+), 54 deletions(-) diff --git a/app/cli.php b/app/cli.php index 51a8e86137..7c2b73e966 100644 --- a/app/cli.php +++ b/app/cli.php @@ -103,12 +103,12 @@ $logError $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - try { - $responseCode = $logger->addLog($log); - Console::info('Error log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - } + try { + $responseCode = $logger->addLog($log); + Console::info('Error log pushed with status code: ' . $responseCode); + } catch (Throwable $th) { + Console::error('Error pushing log: ' . $th->getMessage()); + } } Console::warning("Failed: {$error->getMessage()}"); diff --git a/app/init/constants.php b/app/init/constants.php index f67023ae3e..3d8217e75a 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -149,10 +149,17 @@ const METRIC_MESSAGES_TYPE_PROVIDER_FAILED = METRIC_MESSAGES . '.{type}.{provid const METRIC_SESSIONS = 'sessions'; const METRIC_DATABASES = 'databases'; const METRIC_COLLECTIONS = 'collections'; +const METRIC_DATABASES_STORAGE = 'databases.storage'; +const METRIC_DATABASES_STORAGE_DISK = 'databases.storage_disk'; const METRIC_DATABASE_ID_COLLECTIONS = '{databaseInternalId}.collections'; +const METRIC_DATABASE_ID_STORAGE = '{databaseInternalId}.databases.storage'; +const METRIC_DATABASE_ID_STORAGE_DISK = '{databaseInternalId}.databases.storage_disk'; + const METRIC_DOCUMENTS = 'documents'; const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents'; const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents'; +const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage'; +const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DISK = '{databaseInternalId}.{collectionInternalId}.databases.storage_disk'; const METRIC_BUCKETS = 'buckets'; const METRIC_FILES = 'files'; const METRIC_FILES_STORAGE = 'files.storage'; diff --git a/composer.lock b/composer.lock index cff13d28a1..4603450316 100644 --- a/composer.lock +++ b/composer.lock @@ -2126,16 +2126,16 @@ }, { "name": "utopia-php/logger", - "version": "0.6.0", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9" + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", - "reference": "a2d1daeeb8f61fdec6d851950d9a021a3d05c9f9", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/7e8ff512c6f04577aba1df67c7b9628971946f9c", + "reference": "7e8ff512c6f04577aba1df67c7b9628971946f9c", "shasum": "" }, "require": { @@ -2174,9 +2174,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.6.0" + "source": "https://github.com/utopia-php/logger/tree/0.6.1" }, - "time": "2024-05-23T13:37:54+00:00" + "time": "2024-09-20T14:02:12+00:00" }, { "name": "utopia-php/messaging", @@ -3410,16 +3410,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -3472,7 +3472,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T17:22:50+00:00" }, { "name": "matthiasmullie/minify", @@ -3660,16 +3660,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.3.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", "shasum": "" }, "require": { @@ -3712,9 +3712,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-09-29T13:56:26+00:00" }, { "name": "phar-io/manifest", @@ -4281,16 +4281,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.1", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -4322,9 +4322,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-09-07T20:13:05+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpstan/phpstan", @@ -6020,16 +6020,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -6093,7 +6093,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -6109,7 +6109,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6180,16 +6180,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", - "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", "shasum": "" }, "require": { @@ -6226,7 +6226,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.2" + "source": "https://github.com/symfony/filesystem/tree/v7.1.5" }, "funding": [ { @@ -6242,7 +6242,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/finder", @@ -6691,16 +6691,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -6732,7 +6732,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -6748,7 +6748,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/service-contracts", @@ -6835,16 +6835,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -6902,7 +6902,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -6918,7 +6918,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "textalk/websocket", diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 8dc037897d..ada5e63b07 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -166,7 +166,7 @@ abstract class ScheduleBase extends Action $total = $total + $sum; foreach ($results as $document) { - $localDocument = $this->schedules[$document->getInternalId()] ?? null; + $localDocument = $this->schedules[$document->getInternalId()] ?? null; // Check if resource has been updated since last sync $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; From 5e8ef5523dcc37eae70aa5184a4409af6de13165 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:08:47 -0400 Subject: [PATCH 210/241] Reapply "Fix auth injection" This reverts commit 904e4c86507a26369e409b033336adc3c8d71e09. --- app/controllers/api/avatars.php | 24 ++++++++++++------------ app/controllers/api/vcs.php | 30 +++++++++++++++--------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 54ba096c5d..2b8327e898 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -604,9 +604,9 @@ Http::get('/v1/cards/cloud') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -617,7 +617,7 @@ Http::get('/v1/cards/cloud') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; @@ -812,9 +812,9 @@ Http::get('/v1/cards/cloud-back') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -824,7 +824,7 @@ Http::get('/v1/cards/cloud-back') $userId = $user->getId(); $email = $user->getAttribute('email', ''); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubId = $gitHub['id'] ?? ''; $isHero = \array_key_exists($email, $heroes); @@ -891,9 +891,9 @@ Http::get('/v1/cards/cloud-og') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('auth') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $auth) use ($getUserGitHub) { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->inject('authorization') + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { + $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -908,7 +908,7 @@ Http::get('/v1/cards/cloud-og') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $auth); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 987d84bb7e..ef325ee56b 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -900,9 +900,9 @@ Http::post('/v1/vcs/github/events') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('auth') + ->inject('authorization') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -936,14 +936,14 @@ Http::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -956,13 +956,13 @@ Http::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - $auth->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); + $authorization->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -994,12 +994,12 @@ Http::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1008,7 +1008,7 @@ Http::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = $auth->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1019,7 +1019,7 @@ Http::post('/v1/vcs/github/events') if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1172,15 +1172,15 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('auth') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $auth) use ($createGitDeployments) { + ->inject('authorization') + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = $auth->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = $authorization->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) ])); @@ -1197,7 +1197,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito // TODO: Delete from array when PR is closed - $repository = $auth->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1221,7 +1221,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $auth); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); $response->noContent(); }); From 769a8ee6c90cc408855d5e6813634ce483a946f3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 2 Oct 2024 11:23:27 +0545 Subject: [PATCH 211/241] Remove JPEG fallback for webp - Now that safari supports webp but without the appropriate header, remove the fallback --- app/controllers/api/storage.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index ca248c2628..4e30832a67 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -889,10 +889,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support - $output = 'jpg'; - } - $inputs = Config::getParam('storage-inputs'); $outputs = Config::getParam('storage-outputs'); $fileLogos = Config::getParam('storage-logos'); From 14cd43ddfdc19a2dbb6de732134c900522847cfc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 2 Oct 2024 05:59:46 +0000 Subject: [PATCH 212/241] disable heic --- app/config/storage/outputs.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/storage/outputs.php b/app/config/storage/outputs.php index 8de4f18599..cde2a9f38a 100644 --- a/app/config/storage/outputs.php +++ b/app/config/storage/outputs.php @@ -6,7 +6,7 @@ return [ // Accepted outputs files 'gif' => 'image/gif', 'png' => 'image/png', 'webp' => 'image/webp', - 'heic' => 'image/heic', - 'heics' => 'image/heic', + // 'heic' => 'image/heic', + // 'heics' => 'image/heic', 'avif' => 'image/avif' ]; From d4195aa3a6a988b540a23b4e2abf6e666bd354bb Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 2 Oct 2024 06:02:10 +0000 Subject: [PATCH 213/241] update utopia image --- composer.json | 2 +- composer.lock | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 62efee278e..575973e54d 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.2.*", - "utopia-php/image": "dev-feat-avif-support", + "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", diff --git a/composer.lock b/composer.lock index d0ce3f7384..93672e670e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1981099f3c47f5536298222ba9b0b994", + "content-hash": "66e31af1f7d0d1617694a1c5a975f887", "packages": [ { "name": "adhocore/jwt", @@ -1970,16 +1970,16 @@ }, { "name": "utopia-php/image", - "version": "dev-feat-avif-support", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "7be404a23399dbb06585f8c78ed61a375a090450" + "reference": "fcea143edbad524bf871ddbebe801d981f91f181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/7be404a23399dbb06585f8c78ed61a375a090450", - "reference": "7be404a23399dbb06585f8c78ed61a375a090450", + "url": "https://api.github.com/repos/utopia-php/image/zipball/fcea143edbad524bf871ddbebe801d981f91f181", + "reference": "fcea143edbad524bf871ddbebe801d981f91f181", "shasum": "" }, "require": { @@ -2012,9 +2012,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/feat-avif-support" + "source": "https://github.com/utopia-php/image/tree/0.7.0" }, - "time": "2024-07-29T13:22:58+00:00" + "time": "2024-10-02T05:45:38+00:00" }, { "name": "utopia-php/locale", @@ -6995,9 +6995,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/image": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { From d290460202d85d5d8b58c119e14ed2b54f23c4d8 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 2 Oct 2024 06:06:49 +0000 Subject: [PATCH 214/241] disable heic in mime --- app/config/storage/mimes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/storage/mimes.php b/app/config/storage/mimes.php index a9eaec59d3..df325b37e9 100644 --- a/app/config/storage/mimes.php +++ b/app/config/storage/mimes.php @@ -6,7 +6,7 @@ return [ 'image/gif', 'image/png', 'image/webp', - 'image/heic', + // 'image/heic', 'image/avif', // Video Files From 825c32eb17a1122f97c0c0bf7dd64f5c3b50c2e9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 2 Oct 2024 06:13:09 +0000 Subject: [PATCH 215/241] remove submodule --- app/console | 1 - 1 file changed, 1 deletion(-) delete mode 160000 app/console diff --git a/app/console b/app/console deleted file mode 160000 index 412bc33318..0000000000 --- a/app/console +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 412bc3331891c15ba7baf4f445e82ac3a678c6be From c3b186b3f995432aa039b8c28d2674010c19ef24 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 2 Oct 2024 14:32:56 +0300 Subject: [PATCH 216/241] set setResource project --- app/worker.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/worker.php b/app/worker.php index bb56eca8c1..b356d42ff7 100644 --- a/app/worker.php +++ b/app/worker.php @@ -54,11 +54,16 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register) return $adapter; }, ['cache', 'register']); -Server::setResource('project', function (Message $message) { +Server::setResource('project', function (Message $message, Database $dbForConsole) { $payload = $message->getPayload() ?? []; + $project = new Document($payload['project'] ?? []); - return new Document($payload['project'] ?? []); -}, ['message']); + if ($project->getId() === 'console' || $project->isEmpty() || !empty($project->getInternalId())) { + return $project; + } + + return $dbForConsole->getDocument('projects', $project->getId()); +}, ['message', 'dbForConsole']); Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) { if ($project->isEmpty() || $project->getId() === 'console') { From a408cd3b579972d8071c6bb41c37bc42494a775c Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 2 Oct 2024 14:36:09 +0300 Subject: [PATCH 217/241] set setResource project --- app/worker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/worker.php b/app/worker.php index b356d42ff7..2d59259284 100644 --- a/app/worker.php +++ b/app/worker.php @@ -58,7 +58,7 @@ Server::setResource('project', function (Message $message, Database $dbForConsol $payload = $message->getPayload() ?? []; $project = new Document($payload['project'] ?? []); - if ($project->getId() === 'console' || $project->isEmpty() || !empty($project->getInternalId())) { + if ($project->getId() === 'console' || $project->isEmpty() || ! empty($project->getInternalId())) { return $project; } From a45db60313b52115eadbbfb0ddc3b4d473c71942 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:16:25 +0100 Subject: [PATCH 218/241] fix: falsy values --- tests/e2e/General/UsageTest.php | 4 ++-- .../Functions/FunctionsCustomClientTest.php | 4 ++-- .../Functions/FunctionsCustomServerTest.php | 18 +++++++++--------- .../FunctionsScheduleTest.php | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index db75a5db45..c50a15fd3f 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -931,7 +931,7 @@ class UsageTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'] ], $this->getHeaders()), [ - 'async' => false, + 'async' => 'false', ] ); @@ -955,7 +955,7 @@ class UsageTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'] ], $this->getHeaders()), [ - 'async' => false, + 'async' => 'false', ] ); diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 3447ee0547..31cc05f423 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -97,7 +97,7 @@ class FunctionsCustomClientTest extends Scope $execution = $this->createExecution($functionId, [ 'body' => 'foobar', - 'async' => false + 'async' => 'false' ]); $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); @@ -208,7 +208,7 @@ class FunctionsCustomClientTest extends Scope $execution = $this->createExecution($functionId, [ 'body' => 'foobar', - // Testing default value, should be 'async' => false + // Testing default value, should be 'async' => 'false' ]); $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 14ec660c4b..2c3ddbcf58 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -463,7 +463,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => false + 'activate' => 'false' ]); $this->assertEquals(202, $deployment['headers']['status-code']); @@ -502,7 +502,7 @@ class FunctionsCustomServerTest extends Scope $deployment = $this->createDeployment($functionId, [ 'code' => $this->packageFunction('php'), - 'activate' => false + 'activate' => 'false' ]); $deploymentId = $deployment['body']['$id'] ?? ''; @@ -845,7 +845,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $execution = $this->createExecution($data['functionId'], [ - 'async' => false, + 'async' => 'false', ]); $this->assertEquals(201, $execution['headers']['status-code']); @@ -870,7 +870,7 @@ class FunctionsCustomServerTest extends Scope $executionId = $execution['body']['$id'] ?? ''; $execution = $this->createExecution($data['functionId'], [ - 'async' => false, + 'async' => 'false', 'path' => '/?code=400' ]); @@ -965,7 +965,7 @@ class FunctionsCustomServerTest extends Scope * Test for SUCCESS */ $execution = $this->createExecution($data['functionId'], [ - // Testing default value, should be 'async' => false + // Testing default value, should be 'async' => 'false' ]); $this->assertEquals(201, $execution['headers']['status-code']); @@ -1281,7 +1281,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($functionId, [ 'body' => 'foobar', - 'async' => false + 'async' => 'false' ]); $output = json_decode($execution['body']['responseBody'], true); @@ -1447,7 +1447,7 @@ class FunctionsCustomServerTest extends Scope $execution = $this->createExecution($functionId, [ 'body' => 'foobar', - 'async' => false + 'async' => 'false' ]); $output = json_decode($execution['body']['responseBody'], true); @@ -1561,7 +1561,7 @@ class FunctionsCustomServerTest extends Scope $this->assertStringContainsStringIgnoringCase('"users":', $deployment['body']['buildLogs']); $execution = $this->createExecution($functionId, [ - 'async' => false, + 'async' => 'false', ]); $this->assertEquals(201, $execution['headers']['status-code']); @@ -1611,7 +1611,7 @@ class FunctionsCustomServerTest extends Scope $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; $execution = $this->createExecution($functionId, [ - 'async' => false, + 'async' => 'false', 'headers' => [ 'cookie' => $cookie ] diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index cbe54b6445..b1315103b1 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -143,7 +143,7 @@ class FunctionsScheduleTest extends Scope /* Test for FAILURE */ // Schedule synchronous execution $execution = $this->createExecution($functionId, [ - 'async' => false, + 'async' => 'false', 'scheduledAt' => $futureTime->format(\DateTime::ATOM), ]); $this->assertEquals(400, $execution['headers']['status-code']); From 886687ed34fc275b29bfa6c542419555c85d8320 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:19:15 +0100 Subject: [PATCH 219/241] fix: reorder assertions --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2c3ddbcf58..3af050438f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1450,10 +1450,11 @@ class FunctionsCustomServerTest extends Scope 'async' => 'false' ]); - $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals('completed', $execution['body']['status']); $this->assertEquals(200, $execution['body']['responseStatusCode']); + + $output = json_decode($execution['body']['responseBody'], true); $this->assertEquals(true, $output['v2Woks']); $this->cleanupFunction($functionId); From 48e57be312d1ebeccc833e64f530653530232d35 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 2 Oct 2024 17:18:10 +0200 Subject: [PATCH 220/241] chore: update composer lock --- composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/composer.lock b/composer.lock index 268995100b..ea8044633b 100644 --- a/composer.lock +++ b/composer.lock @@ -1804,7 +1804,7 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/database/tree/0.53.5-rc1", + "source": "https://github.com/utopia-php/database/tree/0.53.5", "issues": "https://github.com/utopia-php/database/issues" }, "time": "2024-09-24T08:43:10+00:00" @@ -2206,16 +2206,16 @@ }, { "name": "utopia-php/migration", - "version": "0.6.1", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0" + "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", - "reference": "6a064179dd0a7278bfbaf65d9abdef19d6d2bbe0", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", + "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", "shasum": "" }, "require": { @@ -2256,9 +2256,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.6.1" + "source": "https://github.com/utopia-php/migration/tree/0.6.4" }, - "time": "2024-09-24T09:54:09+00:00" + "time": "2024-10-02T15:16:36+00:00" }, { "name": "utopia-php/mongo", @@ -3033,16 +3033,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.21", + "version": "0.39.22", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "9754b190d33aaad56fdb8defc94f90248184c5ac" + "reference": "bdbb1607527550e67283ff0533522d1410c2c0df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9754b190d33aaad56fdb8defc94f90248184c5ac", - "reference": "9754b190d33aaad56fdb8defc94f90248184c5ac", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bdbb1607527550e67283ff0533522d1410c2c0df", + "reference": "bdbb1607527550e67283ff0533522d1410c2c0df", "shasum": "" }, "require": { @@ -3078,9 +3078,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.39.21" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.22" }, - "time": "2024-09-10T08:49:29+00:00" + "time": "2024-10-01T16:16:26+00:00" }, { "name": "doctrine/annotations", @@ -3604,16 +3604,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.3.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", "shasum": "" }, "require": { @@ -3656,9 +3656,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-09-29T13:56:26+00:00" }, { "name": "phar-io/manifest", From 671f801cd571ac8826475a63021c8a72109c4598 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:26:45 -0400 Subject: [PATCH 221/241] fix: missing protocol --- app/config/specs/open-api3-1.6.x-client.json | 45 +++---- app/config/specs/open-api3-1.6.x-console.json | 116 +++++++++--------- app/config/specs/open-api3-1.6.x-server.json | 111 ++++++++--------- app/config/specs/open-api3-latest-client.json | 45 +++---- .../specs/open-api3-latest-console.json | 116 +++++++++--------- app/config/specs/open-api3-latest-server.json | 111 ++++++++--------- app/config/specs/swagger2-1.6.x-client.json | 45 +++---- app/config/specs/swagger2-1.6.x-console.json | 116 +++++++++--------- app/config/specs/swagger2-1.6.x-server.json | 111 ++++++++--------- app/config/specs/swagger2-latest-client.json | 45 +++---- app/config/specs/swagger2-latest-console.json | 116 +++++++++--------- app/config/specs/swagger2-latest-server.json | 111 ++++++++--------- app/controllers/api/projects.php | 2 +- 13 files changed, 553 insertions(+), 537 deletions(-) diff --git a/app/config/specs/open-api3-1.6.x-client.json b/app/config/specs/open-api3-1.6.x-client.json index f1b247a70f..021ae27c45 100644 --- a/app/config/specs/open-api3-1.6.x-client.json +++ b/app/config/specs/open-api3-1.6.x-client.json @@ -166,7 +166,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1570,7 +1570,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1802,7 +1802,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -1954,7 +1954,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "File" @@ -2703,7 +2703,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2784,7 +2784,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2873,7 +2873,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -3009,7 +3009,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3088,7 +3088,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3368,7 +3368,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3496,7 +3496,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3628,7 +3628,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3688,7 +3688,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4178,7 +4178,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -4262,7 +4262,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4356,7 +4356,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -5319,7 +5319,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -6001,7 +6001,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -6610,7 +6610,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -7156,7 +7157,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -7343,7 +7344,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -7508,7 +7509,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", diff --git a/app/config/specs/open-api3-1.6.x-console.json b/app/config/specs/open-api3-1.6.x-console.json index 1e3c1a21c4..12163dd464 100644 --- a/app/config/specs/open-api3-1.6.x-console.json +++ b/app/config/specs/open-api3-1.6.x-console.json @@ -206,7 +206,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1591,7 +1591,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1820,7 +1820,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -1972,7 +1972,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "File" @@ -2714,7 +2714,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2795,7 +2795,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2884,7 +2884,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -3020,7 +3020,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3099,7 +3099,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3375,7 +3375,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3503,7 +3503,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3635,7 +3635,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3695,7 +3695,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4185,7 +4185,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -4269,7 +4269,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4363,7 +4363,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -4644,7 +4644,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5523,7 +5523,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5965,7 +5965,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -6073,7 +6073,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -6186,7 +6186,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -6303,7 +6303,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6425,7 +6425,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6543,7 +6543,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6668,7 +6668,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6786,7 +6786,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6911,7 +6911,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -7019,7 +7019,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -7132,7 +7132,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -7265,7 +7265,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7384,7 +7384,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7502,7 +7502,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7610,7 +7610,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7909,7 +7909,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8678,7 +8678,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -9658,7 +9658,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -10373,7 +10373,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -11179,7 +11179,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -12453,7 +12453,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -13208,7 +13208,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -13864,7 +13864,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14169,7 +14169,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14439,7 +14439,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14553,7 +14553,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -17029,7 +17029,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -17463,7 +17463,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17525,7 +17525,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17923,7 +17923,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -22620,7 +22620,8 @@ "description": "Does SMTP server use secure connection", "x-example": "tls", "enum": [ - "tls" + "tls", + "ssl" ], "x-enum-name": "SMTPSecure", "x-enum-keys": [] @@ -25524,7 +25525,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -26133,7 +26134,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -26912,7 +26914,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -27099,7 +27101,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -27264,7 +27266,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -28840,7 +28842,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -29922,7 +29924,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -30611,7 +30613,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/open-api3-1.6.x-server.json b/app/config/specs/open-api3-1.6.x-server.json index 1a394f6def..47fe17866a 100644 --- a/app/config/specs/open-api3-1.6.x-server.json +++ b/app/config/specs/open-api3-1.6.x-server.json @@ -167,7 +167,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1590,7 +1590,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1825,7 +1825,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2370,7 +2370,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2451,7 +2451,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2540,7 +2540,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -2676,7 +2676,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2755,7 +2755,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3039,7 +3039,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3169,7 +3169,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3303,7 +3303,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3365,7 +3365,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3857,7 +3857,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3943,7 +3943,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4039,7 +4039,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -4211,7 +4211,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5026,7 +5026,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5472,7 +5472,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -5581,7 +5581,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -5695,7 +5695,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -5813,7 +5813,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -5936,7 +5936,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6055,7 +6055,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6181,7 +6181,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6300,7 +6300,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6426,7 +6426,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -6535,7 +6535,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -6649,7 +6649,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -6783,7 +6783,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -6903,7 +6903,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7022,7 +7022,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7131,7 +7131,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7433,7 +7433,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8119,7 +8119,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -8767,7 +8767,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -9249,7 +9249,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -10068,7 +10068,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -11279,7 +11279,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -12046,7 +12046,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -12720,7 +12720,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13027,7 +13027,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13299,7 +13299,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13414,7 +13414,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -15915,7 +15915,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -16355,7 +16355,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16418,7 +16418,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16822,7 +16822,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -17516,7 +17516,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -18137,7 +18137,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -18697,7 +18698,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -18888,7 +18889,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -19057,7 +19058,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -20580,7 +20581,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -21677,7 +21678,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -22375,7 +22376,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index f1b247a70f..021ae27c45 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -166,7 +166,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1570,7 +1570,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1802,7 +1802,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -1954,7 +1954,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "File" @@ -2703,7 +2703,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2784,7 +2784,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2873,7 +2873,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -3009,7 +3009,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3088,7 +3088,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3368,7 +3368,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3496,7 +3496,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3628,7 +3628,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3688,7 +3688,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4178,7 +4178,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -4262,7 +4262,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4356,7 +4356,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -5319,7 +5319,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -6001,7 +6001,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -6610,7 +6610,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -7156,7 +7157,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -7343,7 +7344,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -7508,7 +7509,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 1e3c1a21c4..12163dd464 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -206,7 +206,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1591,7 +1591,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1820,7 +1820,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -1972,7 +1972,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "File" @@ -2714,7 +2714,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2795,7 +2795,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2884,7 +2884,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -3020,7 +3020,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3099,7 +3099,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3375,7 +3375,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3503,7 +3503,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3635,7 +3635,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3695,7 +3695,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4185,7 +4185,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -4269,7 +4269,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4363,7 +4363,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -4644,7 +4644,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5523,7 +5523,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5965,7 +5965,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -6073,7 +6073,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -6186,7 +6186,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -6303,7 +6303,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6425,7 +6425,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6543,7 +6543,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6668,7 +6668,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6786,7 +6786,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6911,7 +6911,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -7019,7 +7019,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -7132,7 +7132,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -7265,7 +7265,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7384,7 +7384,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7502,7 +7502,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7610,7 +7610,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7909,7 +7909,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8678,7 +8678,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -9658,7 +9658,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -10373,7 +10373,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -11179,7 +11179,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -12453,7 +12453,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -13208,7 +13208,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -13864,7 +13864,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14169,7 +14169,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14439,7 +14439,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14553,7 +14553,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -17029,7 +17029,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -17463,7 +17463,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17525,7 +17525,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17923,7 +17923,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -22620,7 +22620,8 @@ "description": "Does SMTP server use secure connection", "x-example": "tls", "enum": [ - "tls" + "tls", + "ssl" ], "x-enum-name": "SMTPSecure", "x-enum-keys": [] @@ -25524,7 +25525,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -26133,7 +26134,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -26912,7 +26914,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -27099,7 +27101,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -27264,7 +27266,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -28840,7 +28842,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -29922,7 +29924,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -30611,7 +30613,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 1a394f6def..47fe17866a 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -167,7 +167,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1590,7 +1590,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1825,7 +1825,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2370,7 +2370,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2451,7 +2451,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2540,7 +2540,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "File" @@ -2676,7 +2676,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2755,7 +2755,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3039,7 +3039,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image" @@ -3169,7 +3169,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3303,7 +3303,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3365,7 +3365,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -3857,7 +3857,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image" @@ -3943,7 +3943,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image" @@ -4039,7 +4039,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image" @@ -4211,7 +4211,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5026,7 +5026,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5472,7 +5472,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -5581,7 +5581,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -5695,7 +5695,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -5813,7 +5813,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -5936,7 +5936,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6055,7 +6055,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6181,7 +6181,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6300,7 +6300,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6426,7 +6426,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -6535,7 +6535,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -6649,7 +6649,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -6783,7 +6783,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -6903,7 +6903,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7022,7 +7022,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7131,7 +7131,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7433,7 +7433,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8119,7 +8119,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -8767,7 +8767,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -9249,7 +9249,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -10068,7 +10068,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -11279,7 +11279,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -12046,7 +12046,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -12720,7 +12720,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13027,7 +13027,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13299,7 +13299,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13414,7 +13414,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -15915,7 +15915,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -16355,7 +16355,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16418,7 +16418,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16822,7 +16822,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -17516,7 +17516,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -18137,7 +18137,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -18697,7 +18698,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -18888,7 +18889,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -19057,7 +19058,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -20580,7 +20581,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -21677,7 +21678,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -22375,7 +22376,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/swagger2-1.6.x-client.json b/app/config/specs/swagger2-1.6.x-client.json index cf001af0e8..fce6a871a3 100644 --- a/app/config/specs/swagger2-1.6.x-client.json +++ b/app/config/specs/swagger2-1.6.x-client.json @@ -222,7 +222,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1670,7 +1670,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1915,7 +1915,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2075,7 +2075,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "No content" @@ -2836,7 +2836,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2922,7 +2922,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -3017,7 +3017,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -3152,7 +3152,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3235,7 +3235,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3528,7 +3528,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3657,7 +3657,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3790,7 +3790,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3857,7 +3857,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4348,7 +4348,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4435,7 +4435,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4530,7 +4530,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -5528,7 +5528,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -6225,7 +6225,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -6807,7 +6807,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -7367,7 +7368,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -7556,7 +7557,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -7718,7 +7719,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", diff --git a/app/config/specs/swagger2-1.6.x-console.json b/app/config/specs/swagger2-1.6.x-console.json index d9037abc55..bd2aeaa518 100644 --- a/app/config/specs/swagger2-1.6.x-console.json +++ b/app/config/specs/swagger2-1.6.x-console.json @@ -278,7 +278,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1707,7 +1707,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1949,7 +1949,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2109,7 +2109,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "No content" @@ -2863,7 +2863,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2949,7 +2949,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -3044,7 +3044,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -3179,7 +3179,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3262,7 +3262,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3551,7 +3551,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3680,7 +3680,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3813,7 +3813,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3880,7 +3880,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4371,7 +4371,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4458,7 +4458,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4553,7 +4553,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -4846,7 +4846,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5727,7 +5727,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -6159,7 +6159,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -6265,7 +6265,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -6375,7 +6375,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -6491,7 +6491,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6611,7 +6611,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6729,7 +6729,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6853,7 +6853,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6971,7 +6971,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -7095,7 +7095,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -7201,7 +7201,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -7311,7 +7311,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -7446,7 +7446,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7565,7 +7565,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7681,7 +7681,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7787,7 +7787,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -8075,7 +8075,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8817,7 +8817,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -9806,7 +9806,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -10533,7 +10533,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -11336,7 +11336,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -12660,7 +12660,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -13419,7 +13419,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -14104,7 +14104,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14436,7 +14436,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14728,7 +14728,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14846,7 +14846,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -17461,7 +17461,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -17903,7 +17903,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17965,7 +17965,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -18363,7 +18363,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -23101,7 +23101,8 @@ "default": "", "x-example": "tls", "enum": [ - "tls" + "tls", + "ssl" ], "x-enum-name": "SMTPSecure", "x-enum-keys": [] @@ -26014,7 +26015,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -26596,7 +26597,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -27386,7 +27388,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -27575,7 +27577,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -27737,7 +27739,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -29369,7 +29371,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -30442,7 +30444,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -31133,7 +31135,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/swagger2-1.6.x-server.json b/app/config/specs/swagger2-1.6.x-server.json index 42cf3ff2c7..77ac485772 100644 --- a/app/config/specs/swagger2-1.6.x-server.json +++ b/app/config/specs/swagger2-1.6.x-server.json @@ -238,7 +238,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1705,7 +1705,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1953,7 +1953,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2518,7 +2518,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2604,7 +2604,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2699,7 +2699,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -2834,7 +2834,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2917,7 +2917,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3214,7 +3214,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3345,7 +3345,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3480,7 +3480,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3549,7 +3549,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4042,7 +4042,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4131,7 +4131,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4228,7 +4228,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -4400,7 +4400,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5217,7 +5217,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5653,7 +5653,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -5760,7 +5760,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -5871,7 +5871,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -5988,7 +5988,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6109,7 +6109,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6228,7 +6228,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6353,7 +6353,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6472,7 +6472,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6597,7 +6597,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -6704,7 +6704,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -6815,7 +6815,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -6951,7 +6951,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7071,7 +7071,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7188,7 +7188,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7295,7 +7295,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7586,7 +7586,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8250,7 +8250,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -8917,7 +8917,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -9415,7 +9415,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -10231,7 +10231,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -11494,7 +11494,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -12265,7 +12265,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -12968,7 +12968,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13302,7 +13302,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13596,7 +13596,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13715,7 +13715,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -16355,7 +16355,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -16803,7 +16803,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16866,7 +16866,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17270,7 +17270,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -17981,7 +17981,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -18575,7 +18575,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -19149,7 +19150,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -19342,7 +19343,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -19508,7 +19509,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -21087,7 +21088,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -22175,7 +22176,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -22875,7 +22876,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index cf001af0e8..fce6a871a3 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -222,7 +222,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1670,7 +1670,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1915,7 +1915,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2075,7 +2075,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "No content" @@ -2836,7 +2836,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2922,7 +2922,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -3017,7 +3017,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -3152,7 +3152,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3235,7 +3235,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3528,7 +3528,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3657,7 +3657,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3790,7 +3790,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3857,7 +3857,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4348,7 +4348,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4435,7 +4435,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4530,7 +4530,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -5528,7 +5528,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -6225,7 +6225,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -6807,7 +6807,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -7367,7 +7368,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -7556,7 +7557,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -7718,7 +7719,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index d9037abc55..bd2aeaa518 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -278,7 +278,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1707,7 +1707,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1949,7 +1949,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2109,7 +2109,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "301": { "description": "No content" @@ -2863,7 +2863,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2949,7 +2949,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -3044,7 +3044,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -3179,7 +3179,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -3262,7 +3262,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3551,7 +3551,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3680,7 +3680,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3813,7 +3813,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3880,7 +3880,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4371,7 +4371,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4458,7 +4458,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4553,7 +4553,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -4846,7 +4846,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5727,7 +5727,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -6159,7 +6159,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -6265,7 +6265,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -6375,7 +6375,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -6491,7 +6491,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6611,7 +6611,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6729,7 +6729,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6853,7 +6853,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6971,7 +6971,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -7095,7 +7095,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -7201,7 +7201,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -7311,7 +7311,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -7446,7 +7446,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7565,7 +7565,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7681,7 +7681,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7787,7 +7787,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -8075,7 +8075,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8817,7 +8817,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -9806,7 +9806,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -10533,7 +10533,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -11336,7 +11336,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -12660,7 +12660,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -13419,7 +13419,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -14104,7 +14104,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14436,7 +14436,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14728,7 +14728,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -14846,7 +14846,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -17461,7 +17461,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -17903,7 +17903,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17965,7 +17965,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -18363,7 +18363,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -23101,7 +23101,8 @@ "default": "", "x-example": "tls", "enum": [ - "tls" + "tls", + "ssl" ], "x-enum-name": "SMTPSecure", "x-enum-keys": [] @@ -26014,7 +26015,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -26596,7 +26597,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -27386,7 +27388,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -27575,7 +27577,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -27737,7 +27739,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -29369,7 +29371,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -30442,7 +30444,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -31133,7 +31135,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 42cf3ff2c7..77ac485772 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -238,7 +238,7 @@ "tags": [ "account" ], - "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n", + "description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n", "responses": { "200": { "description": "User", @@ -1705,7 +1705,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", + "description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.", "responses": { "200": { "description": "Token", @@ -1953,7 +1953,7 @@ "tags": [ "account" ], - "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Session", @@ -2518,7 +2518,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2604,7 +2604,7 @@ "tags": [ "account" ], - "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n", + "description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\r\n", "responses": { "201": { "description": "Token", @@ -2699,7 +2699,7 @@ "tags": [ "account" ], - "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "301": { "description": "No content" @@ -2834,7 +2834,7 @@ "tags": [ "account" ], - "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", + "description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.\r\n\r\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).", "responses": { "201": { "description": "Token", @@ -2917,7 +2917,7 @@ "tags": [ "account" ], - "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n", + "description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n", "responses": { "201": { "description": "Token", @@ -3214,7 +3214,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", + "description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.", "responses": { "200": { "description": "Image", @@ -3345,7 +3345,7 @@ "tags": [ "avatars" ], - "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -3480,7 +3480,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -3549,7 +3549,7 @@ "tags": [ "avatars" ], - "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4042,7 +4042,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.", + "description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.", "responses": { "200": { "description": "Image", @@ -4131,7 +4131,7 @@ "tags": [ "avatars" ], - "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n", + "description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n", "responses": { "200": { "description": "Image", @@ -4228,7 +4228,7 @@ "tags": [ "avatars" ], - "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n", + "description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n", "responses": { "200": { "description": "Image", @@ -4400,7 +4400,7 @@ "tags": [ "databases" ], - "description": "Create a new Database.\n", + "description": "Create a new Database.\r\n", "responses": { "201": { "description": "Database", @@ -5217,7 +5217,7 @@ "tags": [ "databases" ], - "description": "Create a boolean attribute.\n", + "description": "Create a boolean attribute.\r\n", "responses": { "202": { "description": "AttributeBoolean", @@ -5653,7 +5653,7 @@ "tags": [ "databases" ], - "description": "Create an email attribute.\n", + "description": "Create an email attribute.\r\n", "responses": { "202": { "description": "AttributeEmail", @@ -5760,7 +5760,7 @@ "tags": [ "databases" ], - "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an email attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEmail", @@ -5871,7 +5871,7 @@ "tags": [ "databases" ], - "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \n", + "description": "Create an enumeration attribute. The `elements` param acts as a white-list of accepted values for this attribute. \r\n", "responses": { "202": { "description": "AttributeEnum", @@ -5988,7 +5988,7 @@ "tags": [ "databases" ], - "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an enum attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeEnum", @@ -6109,7 +6109,7 @@ "tags": [ "databases" ], - "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create a float attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeFloat", @@ -6228,7 +6228,7 @@ "tags": [ "databases" ], - "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a float attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeFloat", @@ -6353,7 +6353,7 @@ "tags": [ "databases" ], - "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\n", + "description": "Create an integer attribute. Optionally, minimum and maximum values can be provided.\r\n", "responses": { "202": { "description": "AttributeInteger", @@ -6472,7 +6472,7 @@ "tags": [ "databases" ], - "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an integer attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeInteger", @@ -6597,7 +6597,7 @@ "tags": [ "databases" ], - "description": "Create IP address attribute.\n", + "description": "Create IP address attribute.\r\n", "responses": { "202": { "description": "AttributeIP", @@ -6704,7 +6704,7 @@ "tags": [ "databases" ], - "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an ip attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeIP", @@ -6815,7 +6815,7 @@ "tags": [ "databases" ], - "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Create relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "202": { "description": "AttributeRelationship", @@ -6951,7 +6951,7 @@ "tags": [ "databases" ], - "description": "Create a string attribute.\n", + "description": "Create a string attribute.\r\n", "responses": { "202": { "description": "AttributeString", @@ -7071,7 +7071,7 @@ "tags": [ "databases" ], - "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update a string attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeString", @@ -7188,7 +7188,7 @@ "tags": [ "databases" ], - "description": "Create a URL attribute.\n", + "description": "Create a URL attribute.\r\n", "responses": { "202": { "description": "AttributeURL", @@ -7295,7 +7295,7 @@ "tags": [ "databases" ], - "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\n", + "description": "Update an url attribute. Changing the `default` value will not update already existing documents.\r\n", "responses": { "200": { "description": "AttributeURL", @@ -7586,7 +7586,7 @@ "tags": [ "databases" ], - "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\n", + "description": "Update relationship attribute. [Learn more about relationship attributes](https:\/\/appwrite.io\/docs\/databases-relationships#relationship-attributes).\r\n", "responses": { "200": { "description": "AttributeRelationship", @@ -8250,7 +8250,7 @@ "tags": [ "databases" ], - "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\nAttributes can be `key`, `fulltext`, and `unique`.", + "description": "Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.\r\nAttributes can be `key`, `fulltext`, and `unique`.", "responses": { "202": { "description": "Index", @@ -8917,7 +8917,7 @@ "tags": [ "functions" ], - "description": "List allowed function specifications for this instance.\n", + "description": "List allowed function specifications for this instance.\r\n", "responses": { "200": { "description": "Specifications List", @@ -9415,7 +9415,7 @@ "tags": [ "functions" ], - "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\n\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\n\nUse the \"command\" param to set the entrypoint used to execute your code.", + "description": "Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.\r\n\r\nThis endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https:\/\/appwrite.io\/docs\/functions).\r\n\r\nUse the \"command\" param to set the entrypoint used to execute your code.", "responses": { "202": { "description": "Deployment", @@ -10231,7 +10231,7 @@ "tags": [ "functions" ], - "description": "Delete a function execution by its unique ID.\n", + "description": "Delete a function execution by its unique ID.\r\n", "responses": { "204": { "description": "No content" @@ -11494,7 +11494,7 @@ "tags": [ "health" ], - "description": "Returns the amount of failed jobs in a given queue.\n", + "description": "Returns the amount of failed jobs in a given queue.\r\n", "responses": { "200": { "description": "Health Queue", @@ -12265,7 +12265,7 @@ "tags": [ "locale" ], - "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", + "description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))", "responses": { "200": { "description": "Locale", @@ -12968,7 +12968,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13302,7 +13302,7 @@ "tags": [ "messaging" ], - "description": "Update a push notification by its unique ID.\n", + "description": "Update a push notification by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13596,7 +13596,7 @@ "tags": [ "messaging" ], - "description": "Update an email message by its unique ID.\n", + "description": "Update an email message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -13715,7 +13715,7 @@ "tags": [ "messaging" ], - "description": "Get a message by its unique ID.\n", + "description": "Get a message by its unique ID.\r\n", "responses": { "200": { "description": "Message", @@ -16355,7 +16355,7 @@ "tags": [ "messaging" ], - "description": "Get a provider by its unique ID.\n", + "description": "Get a provider by its unique ID.\r\n", "responses": { "200": { "description": "Provider", @@ -16803,7 +16803,7 @@ "tags": [ "messaging" ], - "description": "Get a topic by its unique ID.\n", + "description": "Get a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -16866,7 +16866,7 @@ "tags": [ "messaging" ], - "description": "Update a topic by its unique ID.\n", + "description": "Update a topic by its unique ID.\r\n", "responses": { "200": { "description": "Topic", @@ -17270,7 +17270,7 @@ "tags": [ "messaging" ], - "description": "Get a subscriber by its unique ID.\n", + "description": "Get a subscriber by its unique ID.\r\n", "responses": { "200": { "description": "Subscriber", @@ -17981,7 +17981,7 @@ "tags": [ "storage" ], - "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n", + "description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n", "responses": { "201": { "description": "File", @@ -18575,7 +18575,8 @@ "jpeg", "gif", "png", - "webp" + "webp", + "avif" ], "x-enum-name": "ImageFormat", "x-enum-keys": [], @@ -19149,7 +19150,7 @@ "tags": [ "teams" ], - "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n", + "description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n", "responses": { "201": { "description": "Membership", @@ -19342,7 +19343,7 @@ "tags": [ "teams" ], - "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n", + "description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n", "responses": { "200": { "description": "Membership", @@ -19508,7 +19509,7 @@ "tags": [ "teams" ], - "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n", + "description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n", "responses": { "200": { "description": "Membership", @@ -21087,7 +21088,7 @@ "tags": [ "users" ], - "description": "Update the user labels by its unique ID. \n\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", + "description": "Update the user labels by its unique ID. \r\n\r\nLabels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](https:\/\/appwrite.io\/docs\/permissions) for more info.", "responses": { "200": { "description": "User", @@ -22175,7 +22176,7 @@ "tags": [ "users" ], - "description": "Creates a session for a user. Returns an immediately usable session object.\n\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", + "description": "Creates a session for a user. Returns an immediately usable session object.\r\n\r\nIf you want to generate a token for a custom authentication flow, use the [POST \/users\/{userId}\/tokens](https:\/\/appwrite.io\/docs\/server\/users#createToken) endpoint.", "responses": { "201": { "description": "Session", @@ -22875,7 +22876,7 @@ "tags": [ "users" ], - "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\n", + "description": "Returns a token with a secret key for creating a session. Use the user ID and secret and submit a request to the [PUT \/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process.\r\n", "responses": { "201": { "description": "Token", diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index aafd480398..dc93361b57 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1744,7 +1744,7 @@ App::post('/v1/projects/:projectId/smtp/tests') ->param('port', 587, new Integer(), 'SMTP server port', true) ->param('username', '', new Text(0, 0), 'SMTP server username', true) ->param('password', '', new Text(0, 0), 'SMTP server password', true) - ->param('secure', '', new WhiteList(['tls'], true), 'Does SMTP server use secure connection', true) + ->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true) ->inject('response') ->inject('dbForConsole') ->inject('queueForMails') From 04bc24f42b3d179a056f09b49b135695a74e03e1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 3 Oct 2024 11:20:47 +0200 Subject: [PATCH 222/241] tests: fix merge conflict --- tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php index bd2cd9c40a..60c96c6e19 100644 --- a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php @@ -520,9 +520,10 @@ class RealtimeConsoleClientTest extends Scope /** * Test Create Deployment */ + $projectId = $this->getProject()['$id']; $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $projectId, ], $this->getHeaders()), [ 'entrypoint' => 'index.php', 'code' => $this->packageFunction('php'), From ef6ed675b3051bd0b49e0d643860499598c100ab Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:26:31 +0530 Subject: [PATCH 223/241] Fix request response filter tests --- .../Functions/FunctionsCustomServerTest.php | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 571f528805..976553059b 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1791,6 +1791,7 @@ class FunctionsCustomServerTest extends Scope public function testResponseFilters() { + // create function with 1.5.0 response format $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1803,27 +1804,6 @@ class FunctionsCustomServerTest extends Scope 'timeout' => 15, ]); - $this->assertEquals(201, $response['headers']['status-code']); - - $functionId = $function['body']['$id'] ?? ''; - - $this->cleanupFunction($functionId); - } - - public function testRequestFilters() - { - // create function with 1.5.0 response format header - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'runtime' => 'php-8.0', - 'entrypoint' => 'index.php', - 'timeout' => 15, - ]); - $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayNotHasKey('scopes', $response['body']); $this->assertArrayNotHasKey('specification', $response['body']); @@ -1849,6 +1829,27 @@ class FunctionsCustomServerTest extends Scope $this->assertArrayHasKey('scopes', $function['body']); $this->assertArrayHasKey('specification', $function['body']); + $functionId = $function['body']['$id'] ?? ''; + $this->cleanupFunction($functionId); + } + + public function testRequestFilters() + { + // create function + $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 15, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $function1Id = $response['body']['$id']; + // create another function $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', @@ -1862,6 +1863,7 @@ class FunctionsCustomServerTest extends Scope ]); $this->assertEquals(201, $response['headers']['status-code']); + $function2Id = $response['body']['$id']; // list functions using request filters $response = $this->client->call( @@ -1880,6 +1882,9 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertCount(1, $response['body']['functions']); $this->assertEquals('Test2', $response['body']['functions'][0]['name']); + + $this->cleanupFunction($function1Id); + $this->cleanupFunction($function2Id); } public function testFunctionLogging() From 34527efc79add92c10faf0f9f5eec9d1494e90a1 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:07:01 +0530 Subject: [PATCH 224/241] Use helper functions --- .../Functions/FunctionsCustomServerTest.php | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 976553059b..59a3041a3f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1819,11 +1819,7 @@ class FunctionsCustomServerTest extends Scope $this->assertArrayNotHasKey('scopes', $function['body']); $this->assertArrayNotHasKey('specification', $function['body']); - // get function without response format header - $function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + $function = $this->getFunction($function['body']['$id']); $this->assertEquals(200, $function['headers']['status-code']); $this->assertArrayHasKey('scopes', $function['body']); @@ -1835,36 +1831,24 @@ class FunctionsCustomServerTest extends Scope public function testRequestFilters() { - // create function - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function1Id = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'timeout' => 15, + 'execute' => ['any'] ]); - $this->assertEquals(201, $response['headers']['status-code']); - $function1Id = $response['body']['$id']; - - // create another function - $response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + $function2Id = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test2', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'timeout' => 15, + 'execute' => ['any'] ]); - $this->assertEquals(201, $response['headers']['status-code']); - $function2Id = $response['body']['$id']; - // list functions using request filters $response = $this->client->call( Client::METHOD_GET, From 78eb1eec2582a3da01f755049032f6f2c08141c8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 6 Oct 2024 14:15:31 +0300 Subject: [PATCH 225/241] originalId --- app/config/collections.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 1eb286cf8f..0b47862844 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -6094,6 +6094,17 @@ $dbCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('originalId'), + 'type' => Database::VAR_STRING, + 'signed' => true, + 'size' => Database::LENGTH_KEY, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], ], 'indexes' => [ [ From 0a4b86214a6adc172ccd3a30beffe828d1c587f8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 6 Oct 2024 17:20:10 +0300 Subject: [PATCH 226/241] originalId --- app/config/collections.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 0b47862844..be78b06e92 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2406,6 +2406,17 @@ $projectCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('originalId'), + 'type' => Database::VAR_STRING, + 'signed' => true, + 'size' => Database::LENGTH_KEY, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], ], 'indexes' => [ [ @@ -6094,17 +6105,6 @@ $dbCollections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => ID::custom('originalId'), - 'type' => Database::VAR_STRING, - 'signed' => true, - 'size' => Database::LENGTH_KEY, - 'format' => '', - 'filters' => [], - 'required' => true, - 'default' => null, - 'array' => false, - ], ], 'indexes' => [ [ From e87a66c426f9f6b7755d28cf409112f5c6605e5c Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 6 Oct 2024 17:29:35 +0300 Subject: [PATCH 227/241] originalId required false --- app/config/collections.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/collections.php b/app/config/collections.php index be78b06e92..cba3100922 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2413,7 +2413,7 @@ $projectCollections = array_merge([ 'size' => Database::LENGTH_KEY, 'format' => '', 'filters' => [], - 'required' => true, + 'required' => false, 'default' => null, 'array' => false, ], From c06948b266e461b1e141338ed53fcb6126831d00 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:57:07 -0400 Subject: [PATCH 228/241] fixes: phpstan --- src/Appwrite/Platform/Tasks/Specs.php | 10 ---------- .../Services/Functions/FunctionsCustomServerTest.php | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 9d361aa6c0..776410e4e4 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -34,16 +34,6 @@ class Specs extends Action return 'specs'; } - public function getRequest(): UtopiaRequest - { - return new AppwriteRequest(new SwooleRequest()); - } - - public function getResponse(): UtopiaResponse - { - return new AppwriteResponse(new SwooleResponse()); - } - public function __construct() { $this diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 59a3041a3f..908a1e9148 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -553,7 +553,7 @@ class FunctionsCustomServerTest extends Scope $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); From d1729404c1bb80e10a4829e21fb11b027b809421 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:29:35 -0400 Subject: [PATCH 229/241] fix: adjusting test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 908a1e9148..f605564726 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1859,10 +1859,11 @@ class FunctionsCustomServerTest extends Scope 'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax ], $this->getHeaders()), [ - 'queries' => [ 'equal("name", ["Test2"])' ] + 'queries' => [ + Query::equal('name', ['Test2'])->toString(), + ] ] ); - $this->assertEquals(200, $response['headers']['status-code']); $this->assertCount(1, $response['body']['functions']); $this->assertEquals('Test2', $response['body']['functions'][0]['name']); From 160b6c3386835e9e3c7826e732c04502e82c1bcb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:11:39 -0400 Subject: [PATCH 230/241] chore: updates --- composer.json | 8 ++--- composer.lock | 88 ++++++++++++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index 938b53639e..2bc63daaa2 100644 --- a/composer.json +++ b/composer.json @@ -47,13 +47,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.15.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.44.*", + "utopia-php/abuse": "0.46.*", "utopia-php/analytics": "0.13.*", - "utopia-php/audit": "0.44.*", + "utopia-php/audit": "0.46.*", "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.54.*", + "utopia-php/database": "0.55.*", "utopia-php/domains": "0.6.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "1.0.*", @@ -62,7 +62,7 @@ "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.5.*", + "utopia-php/migration": "0.7.*", "utopia-php/orchestration": "0.15.*", "utopia-php/platform": "0.8.*", "utopia-php/view": "0.2.*", diff --git a/composer.lock b/composer.lock index e540fc6c17..2fda8b29c4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7a1cc88504a554200880c4b2ba7cd6e5", + "content-hash": "99f1d21e82fb9944a2b83284f36b88b2", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", "shasum": "" }, "require": { @@ -83,7 +83,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" }, "type": "library", "autoload": { @@ -99,10 +100,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2024-06-26T07:03:23+00:00" }, { "name": "appwrite/php-clamav", @@ -1429,16 +1430,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.44.0", + "version": "0.46.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1" + "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", - "reference": "cd37bba23778f3a0dc54b4ba6ffb36d4d91ed8f1", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/ab09ecf6ceac5420020e6046ec651c155005d3c3", + "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3", "shasum": "" }, "require": { @@ -1446,7 +1447,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.54.*" + "utopia-php/database": "0.55.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1474,9 +1475,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.44.0" + "source": "https://github.com/utopia-php/abuse/tree/0.46.0" }, - "time": "2024-09-05T16:09:32+00:00" + "time": "2024-10-07T19:09:29+00:00" }, { "name": "utopia-php/analytics", @@ -1527,21 +1528,21 @@ }, { "name": "utopia-php/audit", - "version": "0.44.0", + "version": "0.46.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139" + "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/69eee24e4d6cb8fdae31235d80b9a46b18092139", - "reference": "69eee24e4d6cb8fdae31235d80b9a46b18092139", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/660bb322ca1e6a9fa121610a85930e65863e1e5d", + "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.54.*" + "utopia-php/database": "0.55.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1568,9 +1569,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.44.0" + "source": "https://github.com/utopia-php/audit/tree/0.46.0" }, - "time": "2024-09-05T16:12:41+00:00" + "time": "2024-10-07T19:10:12+00:00" }, { "name": "utopia-php/cache", @@ -1726,16 +1727,16 @@ }, { "name": "utopia-php/database", - "version": "0.54.1", + "version": "0.55.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946" + "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/c32d6eab5992c927cbf6fb4aad51d76fc5f64946", - "reference": "c32d6eab5992c927cbf6fb4aad51d76fc5f64946", + "url": "https://api.github.com/repos/utopia-php/database/zipball/6f44079999491feb0ef85686f6b2ac7ef54db828", + "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828", "shasum": "" }, "require": { @@ -1776,9 +1777,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.54.1" + "source": "https://github.com/utopia-php/database/tree/0.55.1" }, - "time": "2024-09-10T10:08:37+00:00" + "time": "2024-10-07T18:52:26+00:00" }, { "name": "utopia-php/di", @@ -2231,27 +2232,34 @@ }, { "name": "utopia-php/migration", - "version": "0.5.3", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6" + "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6", - "reference": "b30e7834da69e25084b0c8e9ba29e4a7b54c6eb6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/aa996ebfd3223fca2aa7d8b9aa31457c95344084", + "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", - "php": "8.*" + "appwrite/appwrite": "11.1.*", + "ext-curl": "*", + "ext-openssl": "*", + "php": "8.3.*", + "utopia-php/database": "0.55.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/storage": "0.19.*" }, "require-dev": { - "laravel/pint": "1.*", - "phpunit/phpunit": "9.*", - "utopia-php/cli": "^0.18.0", - "vlucas/phpdotenv": "5.*" + "ext-pdo": "*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "11.2.*", + "utopia-php/cli": "0.19.*", + "vlucas/phpdotenv": "5.6.*" }, "type": "library", "autoload": { @@ -2273,9 +2281,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.5.3" + "source": "https://github.com/utopia-php/migration/tree/0.7.0" }, - "time": "2024-09-10T10:45:18+00:00" + "time": "2024-10-07T19:00:18+00:00" }, { "name": "utopia-php/mongo", From 3e9a24232c4e6945996ec056db4b60bdf3628364 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:26:01 -0400 Subject: [PATCH 231/241] fix: introducing local variable --- app/controllers/api/account.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a76b9dbea5..007435a5df 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1379,6 +1379,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $email = $oauth2->getUserEmail($accessToken); // Check if this identity is connected to a different user + $sessionUpgrade = false; if (!$user->isEmpty()) { $userId = $user->getId(); @@ -1672,7 +1673,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } - if (isset($sessionUpgrade) && $sessionUpgrade) { + if ($sessionUpgrade) { foreach ($user->getAttribute('targets', []) as $target) { if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { continue; From ab5632bfcd18f948d6de5e37ce48efeefbecd5af Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:17:45 -0400 Subject: [PATCH 232/241] fix: wrong document check --- app/controllers/api/teams.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 6e6ca758c9..633a95c01e 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -463,17 +463,17 @@ Http::post('/v1/teams/:teamId/memberships') $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { + if ($invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') !== $email) { + if ($invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') !== $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); } } - if (empty($invitee)) { // Create new user if no user with same email found + if ($invitee->isEmpty()) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed. @@ -525,6 +525,7 @@ Http::post('/v1/teams/:teamId/memberships') 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), ]))); + } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -572,6 +573,7 @@ Http::post('/v1/teams/:teamId/memberships') $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { try { + print_r($membership); $membership = $dbForProject->createDocument('memberships', $membership); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); From c34f8afa819c92de1d2c843f1aa46bba857eb0e7 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:34:27 -0400 Subject: [PATCH 233/241] fix: wrong document check --- app/controllers/api/account.php | 36 +++++++++---------- app/controllers/api/functions.php | 2 +- app/controllers/api/migrations.php | 10 +++--- app/controllers/api/projects.php | 20 +++++------ app/controllers/api/proxy.php | 2 +- app/controllers/api/teams.php | 2 +- app/controllers/api/users.php | 12 +++---- app/controllers/api/vcs.php | 8 ++--- app/controllers/general.php | 4 +-- .../Platform/Workers/Certificates.php | 6 ++-- src/Appwrite/Platform/Workers/Messaging.php | 4 +-- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 007435a5df..ea0c61a949 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -332,7 +332,7 @@ Http::post('/v1/account') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -395,7 +395,7 @@ Http::post('/v1/account') $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -840,7 +840,7 @@ Http::post('/v1/account/sessions/email') Query::equal('email', [$email]), ]); - if (!$profile || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { + if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -1387,7 +1387,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if (!empty($identityWithMatchingEmail)) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -1418,7 +1418,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if ($session !== false && !$session->isEmpty()) { + if (!$session->isEmpty()) { $user->setAttributes($dbForProject->getDocument('users', $session->getAttribute('userId'))->getArrayCopy()); } } @@ -1436,7 +1436,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $userWithEmail = $dbForProject->findOne('users', [ Query::equal('email', [$email]), ]); - if ($userWithEmail !== false && !$userWithEmail->isEmpty()) { + if (!$userWithEmail->isEmpty()) { $user->setAttributes($userWithEmail->getArrayCopy()); } @@ -1447,7 +1447,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerUid', [$oauth2ID]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { $user = $dbForProject->getDocument('users', $identity->getAttribute('userId')); } } @@ -1467,7 +1467,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -1531,7 +1531,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { // Before creating the identity, check if the email is already associated with another user $userId = $user->getId(); @@ -1816,7 +1816,7 @@ Http::post('/v1/account/tokens/magic-url') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -1833,7 +1833,7 @@ Http::post('/v1/account/tokens/magic-url') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -2058,7 +2058,7 @@ Http::post('/v1/account/tokens/email') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2075,7 +2075,7 @@ Http::post('/v1/account/tokens/email') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -2350,7 +2350,7 @@ Http::post('/v1/account/tokens/phone') $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if ($result !== false && !$result->isEmpty()) { + if (!$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2674,7 +2674,7 @@ Http::patch('/v1/account/email') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -2695,7 +2695,7 @@ Http::patch('/v1/account/email') Query::equal('identifier', [$email]), ])); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } @@ -2866,7 +2866,7 @@ Http::patch('/v1/account/phone') Query::equal('identifier', [$phone]), ])); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } @@ -3027,7 +3027,7 @@ Http::post('/v1/account/recovery') Query::equal('email', [$email]), ]); - if (!$profile) { + if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index d0aa60ad06..77311c8a90 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2297,7 +2297,7 @@ Http::delete('/v1/functions/:functionId/executions/:executionId') Query::equal('active', [true]), ]); - if ($schedule && !$schedule->isEmpty()) { + if (!$schedule->isEmpty()) { $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index f7bd875e3f..5e08e197ae 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -120,7 +120,7 @@ Http::post('/v1/migrations/firebase/oauth') Query::equal('provider', ['firebase']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -569,7 +569,7 @@ Http::get('/v1/migrations/firebase/report/oauth') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -744,7 +744,7 @@ Http::get('/v1/migrations/firebase/redirect') Query::equal('providerEmail', [$email]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -813,7 +813,7 @@ Http::get('/v1/migrations/firebase/projects') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -893,7 +893,7 @@ Http::get('/v1/migrations/firebase/deauthorize') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND } diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index c1b04836d6..8897752f2e 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1077,7 +1077,7 @@ Http::get('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1119,7 +1119,7 @@ Http::put('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1168,7 +1168,7 @@ Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1205,7 +1205,7 @@ Http::delete('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook === false || $webhook->isEmpty()) { + if ($webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1324,7 +1324,7 @@ Http::get('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1360,7 +1360,7 @@ Http::put('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1401,7 +1401,7 @@ Http::delete('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key === false || $key->isEmpty()) { + if ($key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1557,7 +1557,7 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1594,7 +1594,7 @@ Http::put('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1637,7 +1637,7 @@ Http::delete('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform === false || $platform->isEmpty()) { + if ($platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 3ca019c9ce..d4283691ae 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -63,7 +63,7 @@ Http::post('/v1/proxy/rules') Query::equal('domain', [$domain]), ]); - if ($document && !$document->isEmpty()) { + if (!$document->isEmpty()) { if ($document->getAttribute('projectId') === $project->getId()) { $resourceType = $document->getAttribute('resourceType'); $resourceId = $document->getAttribute('resourceId'); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 633a95c01e..5734e96250 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -473,7 +473,7 @@ Http::post('/v1/teams/:teamId/memberships') } } - if ($invitee->isEmpty()) { // Create new user if no user with same email found + if (!isset($invitee) || $invitee->isEmpty()) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed. diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3d080fd98e..9f33ee38db 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -63,7 +63,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } } @@ -140,7 +140,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -164,7 +164,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ]); - if ($existingTarget) { + if (!$existingTarget->isEmpty()) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -1215,7 +1215,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + if (!$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1223,7 +1223,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('identifier', [$email]), ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } @@ -1313,7 +1313,7 @@ Http::patch('/v1/users/:userId/phone') Query::equal('identifier', [$number]), ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index ef325ee56b..1f97e4f2d5 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -108,7 +108,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId Query::orderDesc('$createdAt'), ])); - if ($latestComment !== false && !$latestComment->isEmpty()) { + if (!$latestComment->isEmpty()) { $latestCommentId = $latestComment->getAttribute('providerCommentId', ''); $comment = new Comment(); $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); @@ -369,7 +369,7 @@ Http::get('/v1/vcs/github/callback') $identity = $dbForConsole->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if ($identity !== false && !$identity->isEmpty()) { + if (!$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -416,7 +416,7 @@ Http::get('/v1/vcs/github/callback') Query::equal('projectInternalId', [$projectInternalId]) ]); - if ($installation === false || $installation->isEmpty()) { + if ($installation->isEmpty()) { $teamId = $project->getAttribute('teamId', ''); $installation = new Document([ @@ -724,7 +724,7 @@ Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') Query::equal('provider', ['github']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity === false || $identity->isEmpty()) { + if ($identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } diff --git a/app/controllers/general.php b/app/controllers/general.php index 1e403ef93a..70cf55d280 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -520,7 +520,7 @@ Http::init() $mainDomain = $envDomain; } else { $domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]); - $mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get(); + $mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get(); } if ($mainDomain !== $domain->get()) { @@ -530,7 +530,7 @@ Http::init() Query::equal('domain', [$domain->get()]) ]); - if (!$domainDocument) { + if ($domainDocument->isEmpty()) { $domainDocument = new Document([ 'domain' => $domain->get(), 'resourceType' => 'api', diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 4a8d928ba2..07e1dcdaa6 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -126,7 +126,7 @@ class Certificates extends Action $certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]); // If we don't have certificate for domain yet, let's create new document. At the end we save it - if (!$certificate) { + if ($certificate->isEmpty()) { $certificate = new Document(); $certificate->setAttribute('domain', $domain->get()); } @@ -216,7 +216,7 @@ class Certificates extends Action { // Check if update or insert required $certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]); - if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { + if (!$certificateDocument->isEmpty()) { // Merge new data with current data $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); @@ -478,7 +478,7 @@ class Certificates extends Action Query::equal('domain', [$domain]), ]); - if ($rule !== false && !$rule->isEmpty()) { + if (!$rule->isEmpty()) { $rule->setAttribute('certificateId', $certificateId); $rule->setAttribute('status', $success ? 'verified' : 'unverified'); $dbForConsole->updateDocument('rules', $rule->getId(), $rule); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 510fec0431..ddb72803e3 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -178,7 +178,7 @@ class Messaging extends Action Query::equal('type', [$providerType]), ]); - if ($default === false || $default->isEmpty()) { + if ($default->isEmpty()) { $dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([ 'status' => MessageStatus::FAILED, 'deliveryErrors' => ['No enabled provider found.'] @@ -275,7 +275,7 @@ class Messaging extends Action Query::equal('identifier', [$result['recipient']]) ]); - if ($target instanceof Document && !$target->isEmpty()) { + if (!$target->isEmpty()) { $dbForProject->updateDocument( 'targets', $target->getId(), From 60e0554fdf0c4622bc3220d65d26403ae5358673 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:41:44 -0400 Subject: [PATCH 234/241] fix: remove leftover print_r --- app/controllers/api/teams.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 5734e96250..8f5fba30ab 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -573,7 +573,6 @@ Http::post('/v1/teams/:teamId/memberships') $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { try { - print_r($membership); $membership = $dbForProject->createDocument('memberships', $membership); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); From 40266481e0232f6162b4018270fa211667fed8f3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:48:55 -0400 Subject: [PATCH 235/241] chore: package update --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2bc63daaa2..135822bc9e 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.19.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.55.*", + "utopia-php/database": "0.55.1", "utopia-php/domains": "0.6.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "1.0.*", diff --git a/composer.lock b/composer.lock index 2fda8b29c4..cb468c1663 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "99f1d21e82fb9944a2b83284f36b88b2", + "content-hash": "b808f001c3ff4c0396cf51f46e8b314a", "packages": [ { "name": "adhocore/jwt", From 25650e7501b8dba8a2a3a5de96283dd4f2e32b89 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:01:59 -0400 Subject: [PATCH 236/241] chore: trigger ci From 61dba4eefb77b67e7f61befb9a0de331c5771741 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:16:31 -0400 Subject: [PATCH 237/241] revert: reverting commit 39490e9 --- .github/workflows/tests.yml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e7012527d..3ca3c9c833 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,22 +16,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v2 - name: Build Appwrite - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v3 with: context: . push: false tags: ${{ env.IMAGE }} load: true - cache-from: type=gha,scope=appwrite - cache-to: type=gha,mode=max,scope=appwrite + cache-from: type=gha + cache-to: type=gha,mode=max outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar build-args: | DEBUG=false @@ -39,13 +39,12 @@ jobs: VERSION=dev - name: Cache Docker Image - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} - restore-keys: | - appwrite-dev- path: /tmp/${{ env.IMAGE }}.tar + unit_test: name: Unit Test runs-on: ubuntu-latest @@ -53,10 +52,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -83,10 +82,10 @@ jobs: needs: setup steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -131,10 +130,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -158,9 +157,9 @@ jobs: needs: setup steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Load Cache - uses: actions/cache@v4 + uses: actions/cache@v3 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar From 8bc37026cd9fbdcc820c5c12378eba8995a33f21 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 8 Oct 2024 20:54:40 +1300 Subject: [PATCH 238/241] Revert "Feat adding coroutines" --- .github/workflows/codeql-phpstan.yml | 16 - .github/workflows/tests.yml | 31 +- app/cli.php | 272 ++- app/config/variables.php | 2 +- app/controllers/api/account.php | 616 +++--- app/controllers/api/avatars.php | 102 +- app/controllers/api/console.php | 12 +- app/controllers/api/databases.php | 427 ++-- app/controllers/api/functions.php | 213 +- app/controllers/api/graphql.php | 29 +- app/controllers/api/health.php | 340 ++- app/controllers/api/locale.php | 20 +- app/controllers/api/messaging.php | 181 +- app/controllers/api/migrations.php | 64 +- app/controllers/api/project.php | 35 +- app/controllers/api/projects.php | 294 ++- app/controllers/api/proxy.php | 22 +- app/controllers/api/storage.php | 211 +- app/controllers/api/teams.php | 115 +- app/controllers/api/users.php | 123 +- app/controllers/api/vcs.php | 97 +- app/controllers/general.php | 267 +-- app/controllers/mock.php | 34 +- app/controllers/shared/api.php | 122 +- app/controllers/shared/api/auth.php | 22 +- app/controllers/web/console.php | 6 +- app/controllers/web/home.php | 4 +- app/http.php | 501 +++-- app/init.php | 1867 +++++++++++++++-- app/init/config.php | 37 - app/init/constants.php | 202 -- app/init/database/filters.php | 397 ---- app/init/database/formats.php | 43 - app/init/locale.php | 23 - app/init/resources.php | 1091 ---------- app/realtime.php | 277 ++- app/views/install/compose.phtml | 5 +- app/worker.php | 363 +++- composer.json | 39 +- composer.lock | 505 ++--- docker-compose.yml | 6 +- docs/tutorials/add-route.md | 30 +- phpstan.neon | 11 - src/Appwrite/Auth/Auth.php | 36 +- src/Appwrite/Auth/Authentication.php | 57 - src/Appwrite/Auth/Validator/MockNumber.php | 5 +- src/Appwrite/Auth/Validator/Password.php | 2 +- src/Appwrite/Auth/Validator/Phone.php | 2 +- src/Appwrite/Event/Func.php | 4 +- src/Appwrite/Event/Validator/Event.php | 2 +- src/Appwrite/Functions/Validator/Headers.php | 2 +- src/Appwrite/Functions/Validator/Payload.php | 2 +- .../Validator/RuntimeSpecification.php | 2 +- src/Appwrite/GraphQL/Resolvers.php | 170 +- src/Appwrite/GraphQL/Schema.php | 119 +- src/Appwrite/GraphQL/Types/Mapper.php | 64 +- src/Appwrite/Migration/Migration.php | 36 +- src/Appwrite/Network/Validator/CNAME.php | 2 +- src/Appwrite/Network/Validator/Email.php | 4 +- src/Appwrite/Network/Validator/Origin.php | 4 +- src/Appwrite/Platform/Tasks/Doctor.php | 104 +- src/Appwrite/Platform/Tasks/Install.php | 11 +- src/Appwrite/Platform/Tasks/Migrate.php | 22 +- src/Appwrite/Platform/Tasks/QueueCount.php | 4 +- src/Appwrite/Platform/Tasks/QueueRetry.php | 4 +- src/Appwrite/Platform/Tasks/SSL.php | 4 +- src/Appwrite/Platform/Tasks/ScheduleBase.php | 136 +- .../Platform/Tasks/ScheduleExecutions.php | 30 +- .../Platform/Tasks/ScheduleFunctions.php | 17 +- .../Platform/Tasks/ScheduleMessages.php | 24 +- src/Appwrite/Platform/Tasks/Specs.php | 65 +- src/Appwrite/Platform/Tasks/Upgrade.php | 4 +- src/Appwrite/Platform/Workers/Audits.php | 6 +- src/Appwrite/Platform/Workers/Builds.php | 66 +- .../Platform/Workers/Certificates.php | 36 +- src/Appwrite/Platform/Workers/Deletes.php | 14 +- src/Appwrite/Platform/Workers/Mails.php | 6 +- src/Appwrite/Platform/Workers/Messaging.php | 4 +- src/Appwrite/Promises/Promise.php | 2 +- src/Appwrite/Promises/Swoole.php | 17 +- src/Appwrite/Specification/Format.php | 10 +- .../Specification/Format/OpenAPI3.php | 53 +- .../Specification/Format/Swagger2.php | 42 +- src/Appwrite/Task/Validator/Cron.php | 2 +- .../Utopia/Database/Validator/CompoundUID.php | 2 +- .../Utopia/Database/Validator/ProjectId.php | 2 +- src/Appwrite/Utopia/Queue/Connections.php | 36 - src/Appwrite/Utopia/Request.php | 38 +- src/Appwrite/Utopia/Response.php | 318 ++- src/Appwrite/Utopia/Response/Models.php | 304 --- src/Appwrite/Utopia/View.php | 2 +- .../e2e/Services/Databases/DatabasesBase.php | 4 +- .../Databases/DatabasesCustomClientTest.php | 4 +- .../DatabasesPermissionsGuestTest.php | 20 +- .../e2e/Services/Functions/FunctionsBase.php | 6 +- .../Functions/FunctionsCustomServerTest.php | 7 +- .../FunctionsScheduleTest.php | 3 +- tests/e2e/Services/GraphQL/Base.php | 2 +- .../Services/GraphQL/StorageClientTest.php | 1 + .../Services/GraphQL/StorageServerTest.php | 1 + .../Projects/ProjectsConsoleClientTest.php | 2 +- .../Storage/StorageCustomClientTest.php | 4 +- .../Webhooks/WebhooksCustomServerTest.php | 5 +- tests/resources/docker/docker-compose.yml | 2 +- tests/unit/Auth/AuthTest.php | 35 +- tests/unit/Event/EventTest.php | 10 +- tests/unit/GraphQL/BuilderTest.php | 9 +- .../unit/Messaging/MessagingChannelsTest.php | 9 +- tests/unit/Migration/MigrationTest.php | 2 +- tests/unit/Utopia/RequestTest.php | 7 +- tests/unit/Utopia/ResponseTest.php | 10 +- 111 files changed, 5230 insertions(+), 5885 deletions(-) delete mode 100644 .github/workflows/codeql-phpstan.yml delete mode 100644 app/init/config.php delete mode 100644 app/init/constants.php delete mode 100644 app/init/database/filters.php delete mode 100644 app/init/database/formats.php delete mode 100644 app/init/locale.php delete mode 100644 app/init/resources.php delete mode 100644 phpstan.neon delete mode 100644 src/Appwrite/Auth/Authentication.php delete mode 100644 src/Appwrite/Utopia/Queue/Connections.php delete mode 100644 src/Appwrite/Utopia/Response/Models.php diff --git a/.github/workflows/codeql-phpstan.yml b/.github/workflows/codeql-phpstan.yml deleted file mode 100644 index 3253e2c38b..0000000000 --- a/.github/workflows/codeql-phpstan.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "CodeQL" - -on: [pull_request] -jobs: - lint: - name: CodeQL - runs-on: ubuntu-latest - - steps: - - name: Check out the repo - uses: actions/checkout@v2 - - - name: Run CodeQL - run: | - docker run --rm -v $PWD:/app composer sh -c \ - "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ca3c9c833..6e7012527d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,22 +16,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build Appwrite - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . push: false tags: ${{ env.IMAGE }} load: true - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=appwrite + cache-to: type=gha,mode=max,scope=appwrite outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar build-args: | DEBUG=false @@ -39,12 +39,13 @@ jobs: VERSION=dev - name: Cache Docker Image - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} + restore-keys: | + appwrite-dev- path: /tmp/${{ env.IMAGE }}.tar - unit_test: name: Unit Test runs-on: ubuntu-latest @@ -52,10 +53,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -82,10 +83,10 @@ jobs: needs: setup steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -130,10 +131,10 @@ jobs: steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar @@ -157,9 +158,9 @@ jobs: needs: setup steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: key: ${{ env.CACHE_KEY }} path: /tmp/${{ env.IMAGE }}.tar diff --git a/app/cli.php b/app/cli.php index 7c2b73e966..23502ec402 100644 --- a/app/cli.php +++ b/app/cli.php @@ -7,134 +7,214 @@ use Appwrite\Event\Delete; use Appwrite\Event\Func; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; -use Swoole\Runtime; -use Utopia\CLI\Adapters\Swoole as SwooleCLI; +use Utopia\Cache\Adapter\Sharding; +use Utopia\Cache\Cache; +use Utopia\CLI\CLI; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DI\Dependency; +use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; +use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Registry\Registry; use Utopia\System\System; -global $registry, $container; - -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - // overwriting runtimes to be architectur agnostic for CLI Config::setParam('runtimes', (new Runtimes('v4'))->getAll(supported: false)); // require controllers after overwriting runtimes require_once __DIR__ . '/controllers/general.php'; -/** - * @var Registry $registry - * @var Container $container - */ -$context = new Dependency(); -$register = new Dependency(); -$logError = new Dependency(); -$queueForDeletes = new Dependency(); -$queueForFunctions = new Dependency(); -$queueForCertificates = new Dependency(); +Authorization::disable(); -$context - ->setName('context') - ->setCallback(fn () => $container); +CLI::setResource('register', fn () => $register); -$register - ->setName('register') - ->setCallback(function () use (&$registry): Registry { - return $registry; - }); +CLI::setResource('cache', function ($pools) { + $list = Config::getParam('pools-cache', []); + $adapters = []; -$queueForFunctions - ->setName('queueForFunctions') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Func($queue); - }); + foreach ($list as $value) { + $adapters[] = $pools + ->get($value) + ->pop() + ->getResource() + ; + } + return new Cache(new Sharding($adapters)); +}, ['pools']); -$queueForDeletes - ->setName('queueForDeletes') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Delete($queue); - }); +CLI::setResource('pools', function (Registry $register) { + return $register->get('pools'); +}, ['register']); -$queueForCertificates - ->setName('queueForCertificates') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Certificate($queue); - }); +CLI::setResource('dbForConsole', function ($pools, $cache) { + $sleep = 3; + $maxAttempts = 5; + $attempts = 0; + $ready = false; -$logError - ->setName('logError') - ->inject('register') - ->setCallback(function (Registry $register) { - return function (Throwable $error, string $namespace, string $action) use ($register) { - $logger = $register->get('logger'); + do { + $attempts++; + try { + // Prepare database connection + $dbAdapter = $pools + ->get('console') + ->pop() + ->getResource(); - if ($logger) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + $dbForConsole = new Database($dbAdapter, $cache); - $log = new Log(); - $log->setNamespace($namespace); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); + $dbForConsole + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console'); - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', get_class($error)); + // Ensure tables exist + $collections = Config::getParam('collections', [])['console']; + $last = \array_key_last($collections); - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('trace', $error->getTraceAsString()); - - $log->setAction($action); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - try { - $responseCode = $logger->addLog($log); - Console::info('Error log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - } + if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */ + throw new Exception('Tables not ready yet.'); } - Console::warning("Failed: {$error->getMessage()}"); - Console::warning($error->getTraceAsString()); - }; - }); + $ready = true; + } catch (\Throwable $err) { + Console::warning($err->getMessage()); + $pools->get('console')->reclaim(); + sleep($sleep); + } + } while ($attempts < $maxAttempts && !$ready); -$container->set($context); -$container->set($logError); -$container->set($register); -$container->set($queueForDeletes); -$container->set($queueForFunctions); -$container->set($queueForCertificates); + if (!$ready) { + throw new Exception("Console is not ready yet. Please try again later."); + } + + return $dbForConsole; +}, ['pools', 'cache']); + +CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if (isset($databases[$dsn->getHost()])) { + $database = $databases[$dsn->getHost()]; + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; + } + + $dbAdapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); + + $database = new Database($dbAdapter, $cache); + + $databases[$dsn->getHost()] = $database; + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + $database + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()); + + return $database; + }; +}, ['pools', 'dbForConsole', 'cache']); + +CLI::setResource('queue', function (Group $pools) { + return $pools->get('queue')->pop()->getResource(); +}, ['pools']); +CLI::setResource('queueForFunctions', function (Connection $queue) { + return new Func($queue); +}, ['queue']); +CLI::setResource('queueForDeletes', function (Connection $queue) { + return new Delete($queue); +}, ['queue']); +CLI::setResource('queueForCertificates', function (Connection $queue) { + return new Certificate($queue); +}, ['queue']); +CLI::setResource('logError', function (Registry $register) { + return function (Throwable $error, string $namespace, string $action) use ($register) { + $logger = $register->get('logger'); + + if ($logger) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace($namespace); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', get_class($error)); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + + $log->setAction($action); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + try { + $responseCode = $logger->addLog($log); + Console::info('Error log pushed with status code: ' . $responseCode); + } catch (Throwable $th) { + Console::error('Error pushing log: ' . $th->getMessage()); + } + } + + Console::warning("Failed: {$error->getMessage()}"); + Console::warning($error->getTraceAsString()); + }; +}, ['register']); $platform = new Appwrite(); -$platform->init(Service::TYPE_TASK, ['adapter' => new SwooleCLI(1)]); +$platform->init(Service::TYPE_TASK); $cli = $platform->getCli(); -$cli - ->init() - ->inject('authorization') - ->action(function (Authorization $authorization) { - $authorization->disable(); - }); - $cli ->error() ->inject('error') @@ -142,6 +222,4 @@ $cli Console::error($error->getMessage()); }); -$cli - ->setContainer($container) - ->run(); +$cli->run(); diff --git a/app/config/variables.php b/app/config/variables.php index 161951e3de..113fbae335 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -254,7 +254,7 @@ return [ 'name' => '_APP_WORKER_PER_CORE', 'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.', 'introduction' => '0.13.0', - 'default' => 2, + 'default' => 6, 'required' => false, 'question' => '', 'filter' => '' diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index ea0c61a949..64441fee5c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,7 +2,6 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; -use Appwrite\Auth\Authentication; use Appwrite\Auth\MFA\Challenge; use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; @@ -29,6 +28,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Identities; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Audit\Audit as EventAudit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -45,16 +45,15 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Assoc; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Host; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\URL; -use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Assoc; +use Utopia\Validator\Boolean; +use Utopia\Validator\Host; +use Utopia\Validator\Text; +use Utopia\Validator\URL; +use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; @@ -145,13 +144,14 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc ->trigger(); }; -$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Authorization $authorization, Authentication $authentication) { - $roles = $authorization->getRoles(); + +$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails) { + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); /** @var Utopia\Database\Document $user */ - $userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); + $userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); if ($userFromRequest->isEmpty()) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -197,7 +197,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $detector->getDevice() )); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session ->setAttribute('$permissions', [ @@ -206,7 +206,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res Permission::delete(Role::user($user->getId())), ])); - $authorization->skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); + Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); $dbForProject->purgeCachedDocument('users', $user->getId()); // Magic URL + Email OTP @@ -247,15 +247,15 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setParam('sessionId', $session->getId()); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $sessionSecret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $protocol = $request->getProtocol(); $response - ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -270,7 +270,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res $response->dynamic($session, Response::MODEL_SESSION); }; -Http::post('/v1/account') +App::post('/v1/account') ->desc('Create account') ->groups(['api', 'account', 'auth']) ->label('event', 'users.[userId].create') @@ -298,8 +298,7 @@ Http::post('/v1/account') ->inject('dbForProject') ->inject('queueForEvents') ->inject('hooks') - ->inject('authorization') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -332,7 +331,7 @@ Http::post('/v1/account') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -377,9 +376,9 @@ Http::post('/v1/account') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + $user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -395,7 +394,7 @@ Http::post('/v1/account') $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if (!$existingTarget->isEmpty()) { + if ($existingTarget) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -405,9 +404,9 @@ Http::post('/v1/account') throw new Exception(Exception::USER_ALREADY_EXISTS); } - $authorization->removeRole(Role::guests()->toString()); - $authorization->addRole(Role::user($user->getId())->toString()); - $authorization->addRole(Role::users()->toString()); + Authorization::unsetRole(Role::guests()->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::users()->toString()); $queueForEvents->setParam('userId', $user->getId()); @@ -416,7 +415,7 @@ Http::post('/v1/account') ->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::get('/v1/account') +App::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) ->label('scope', 'account') @@ -439,7 +438,7 @@ Http::get('/v1/account') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::delete('/v1/account') +App::delete('/v1/account') ->desc('Delete account') ->groups(['api', 'account']) ->label('event', 'users.[userId].delete') @@ -487,7 +486,7 @@ Http::delete('/v1/account') $response->noContent(); }); -Http::get('/v1/account/sessions') +App::get('/v1/account/sessions') ->desc('List sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -502,16 +501,15 @@ Http::get('/v1/account/sessions') ->inject('response') ->inject('user') ->inject('locale') - ->inject('authorization') - ->inject('authentication') - ->action(function (Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { + ->inject('project') + ->action(function (Response $response, Document $user, Locale $locale, Document $project) { - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, $authentication->getSecret()); + $current = Auth::sessionVerify($sessions, Auth::$secret); foreach ($sessions as $key => $session) {/** @var Document $session */ $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -529,7 +527,7 @@ Http::get('/v1/account/sessions') ]), Response::MODEL_SESSION_LIST); }); -Http::delete('/v1/account/sessions') +App::delete('/v1/account/sessions') ->desc('Delete sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -550,8 +548,7 @@ Http::delete('/v1/account/sessions') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->inject('authentication') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -567,13 +564,13 @@ Http::delete('/v1/account/sessions') ->setAttribute('current', false) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { $session->setAttribute('current', true); // If current session delete the cookies too $response - ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $queueForEvents @@ -595,7 +592,7 @@ Http::delete('/v1/account/sessions') $response->noContent(); }); -Http::get('/v1/account/sessions/:sessionId') +App::get('/v1/account/sessions/:sessionId') ->desc('Get session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -612,17 +609,16 @@ Http::get('/v1/account/sessions/:sessionId') ->inject('response') ->inject('user') ->inject('locale') - ->inject('authorization') - ->inject('authentication') - ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Authorization $authorization, Authentication $authentication) { + ->inject('project') + ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Document $project) { - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $sessions = $user->getAttribute('sessions', []); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) + ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) : $sessionId; foreach ($sessions as $session) {/** @var Document $session */ @@ -630,7 +626,7 @@ Http::get('/v1/account/sessions/:sessionId') $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); $session - ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash($authentication->getSecret()))) + ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) ->setAttribute('countryName', $countryName) ->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '') ; @@ -642,7 +638,7 @@ Http::get('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -Http::delete('/v1/account/sessions/:sessionId') +App::delete('/v1/account/sessions/:sessionId') ->desc('Delete session') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') @@ -665,12 +661,12 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->inject('authentication') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Authentication $authentication) { + ->inject('project') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Document $project) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) + ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -689,7 +685,7 @@ Http::delete('/v1/account/sessions/:sessionId') $session->setAttribute('current', false); - if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $session ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); @@ -699,8 +695,8 @@ Http::delete('/v1/account/sessions/:sessionId') } $response - ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -722,7 +718,7 @@ Http::delete('/v1/account/sessions/:sessionId') throw new Exception(Exception::USER_SESSION_NOT_FOUND); }); -Http::patch('/v1/account/sessions/:sessionId') +App::patch('/v1/account/sessions/:sessionId') ->desc('Update session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -744,11 +740,10 @@ Http::patch('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->inject('authentication') - ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Authentication $authentication) { + ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) { $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()) + ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -799,7 +794,7 @@ Http::patch('/v1/account/sessions/:sessionId') return $response->dynamic($session, Response::MODEL_SESSION); }); -Http::post('/v1/account/sessions/email') +App::post('/v1/account/sessions/email') ->alias('/v1/account/sessions') ->desc('Create email password session') ->groups(['api', 'account', 'auth', 'session']) @@ -830,9 +825,7 @@ Http::post('/v1/account/sessions/email') ->inject('queueForEvents') ->inject('queueForMails') ->inject('hooks') - ->inject('authorization') - ->inject('authentication') - ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Authorization $authorization, Authentication $authentication) { + ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -840,7 +833,7 @@ Http::post('/v1/account/sessions/email') Query::equal('email', [$email]), ]); - if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { + if (!$profile || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -848,7 +841,7 @@ Http::post('/v1/account/sessions/email') throw new Exception(Exception::USER_BLOCKED); // User is in status blocked } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -879,7 +872,7 @@ Http::post('/v1/account/sessions/email') $detector->getDevice() )); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); // Re-hash if not using recommended algo if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) { @@ -900,15 +893,15 @@ Http::post('/v1/account/sessions/email') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -936,7 +929,7 @@ Http::post('/v1/account/sessions/email') $response->dynamic($session, Response::MODEL_SESSION); }); -Http::post('/v1/account/sessions/anonymous') +App::post('/v1/account/sessions/anonymous') ->desc('Create anonymous session') ->groups(['api', 'account', 'auth', 'session']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -962,11 +955,9 @@ Http::post('/v1/account/sessions/anonymous') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->inject('authorization') - ->inject('authentication') - ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { + ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents) { $protocol = $request->getProtocol(); - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -1012,7 +1003,7 @@ Http::post('/v1/account/sessions/anonymous') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); // Create session token $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; @@ -1038,7 +1029,7 @@ Http::post('/v1/account/sessions/anonymous') $detector->getDevice() )); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [ Permission::read(Role::user($user->getId())), @@ -1054,14 +1045,14 @@ Http::post('/v1/account/sessions/anonymous') ; if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1076,7 +1067,7 @@ Http::post('/v1/account/sessions/anonymous') $response->dynamic($session, Response::MODEL_SESSION); }); -Http::post('/v1/account/sessions/token') +App::post('/v1/account/sessions/token') ->desc('Create session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -1104,11 +1095,9 @@ Http::post('/v1/account/sessions/token') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->inject('authentication') ->action($createSession); -Http::get('/v1/account/sessions/oauth2/:provider') +App::get('/v1/account/sessions/oauth2/:provider') ->desc('Create OAuth2 session') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1178,7 +1167,7 @@ Http::get('/v1/account/sessions/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -Http::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') +App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1208,7 +1197,7 @@ Http::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -Http::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') +App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 callback') ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1239,7 +1228,7 @@ Http::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') . \http_build_query($params)); }); -Http::get('/v1/account/sessions/oauth2/:provider/redirect') +App::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 redirect') ->groups(['api', 'account', 'session']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1263,9 +1252,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->inject('authorization') - ->inject('authentication') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) { $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; @@ -1379,7 +1366,6 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $email = $oauth2->getUserEmail($accessToken); // Check if this identity is connected to a different user - $sessionUpgrade = false; if (!$user->isEmpty()) { $userId = $user->getId(); @@ -1387,7 +1373,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if (!empty($identityWithMatchingEmail)) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -1403,7 +1389,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') } $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, $authentication->getSecret()); + $current = Auth::sessionVerify($sessions, Auth::$secret); if ($current) { // Delete current session of new one. $currentDocument = $dbForProject->getDocument('sessions', $current); @@ -1418,7 +1404,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if (!$session->isEmpty()) { + if ($session !== false && !$session->isEmpty()) { $user->setAttributes($dbForProject->getDocument('users', $session->getAttribute('userId'))->getArrayCopy()); } } @@ -1436,7 +1422,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $userWithEmail = $dbForProject->findOne('users', [ Query::equal('email', [$email]), ]); - if (!$userWithEmail->isEmpty()) { + if ($userWithEmail !== false && !$userWithEmail->isEmpty()) { $user->setAttributes($userWithEmail->getArrayCopy()); } @@ -1447,7 +1433,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('providerUid', [$oauth2ID]), ]); - if (!$identity->isEmpty()) { + if ($identity !== false && !$identity->isEmpty()) { $user = $dbForProject->getDocument('users', $identity->getAttribute('userId')); } } @@ -1467,7 +1453,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -1500,16 +1486,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); - + $userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), ], - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), + 'userId' => $userDoc->getId(), + 'userInternalId' => $userDoc->getInternalId(), 'providerType' => MESSAGE_TYPE_EMAIL, 'identifier' => $email, ])); @@ -1519,8 +1504,8 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') } } - $authorization->addRole(Role::user($user->getId())->toString()); - $authorization->addRole(Role::users()->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::users()->toString()); if (false === $user->getAttribute('status')) { // Account is blocked $failureRedirect(Exception::USER_BLOCKED); // User is in status blocked @@ -1531,7 +1516,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') Query::equal('provider', [$provider]), Query::equal('providerUid', [$oauth2ID]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { // Before creating the identity, check if the email is already associated with another user $userId = $user->getId(); @@ -1579,7 +1564,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->updateDocument('users', $user->getId(), $user); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); @@ -1601,7 +1586,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -1651,7 +1636,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') $session->setAttribute('expire', $expire); if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); } $queueForEvents @@ -1664,16 +1649,16 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); $query['domain'] = Config::getParam('cookieDomain'); - $query['key'] = $authentication->getCookieName(); + $query['key'] = Auth::$cookieName; $query['secret'] = Auth::encodeSession($user->getId(), $secret); } $response - ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } - if ($sessionUpgrade) { + if (isset($sessionUpgrade) && $sessionUpgrade) { foreach ($user->getAttribute('targets', []) as $target) { if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { continue; @@ -1701,7 +1686,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); -Http::get('/v1/account/tokens/oauth2/:provider') +App::get('/v1/account/tokens/oauth2/:provider') ->desc('Create OAuth2 token') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -1770,7 +1755,7 @@ Http::get('/v1/account/tokens/oauth2/:provider') ->redirect($oauth2->getLoginURL()); }); -Http::post('/v1/account/tokens/magic-url') +App::post('/v1/account/tokens/magic-url') ->alias('/v1/account/sessions/magic-url') ->desc('Create magic URL token') ->groups(['api', 'account', 'auth']) @@ -1800,8 +1785,7 @@ Http::post('/v1/account/tokens/magic-url') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -1811,12 +1795,12 @@ Http::post('/v1/account/tokens/magic-url') $phrase = Phrase::generate(); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if (!$result->isEmpty()) { + if ($result !== false && !$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -1833,7 +1817,7 @@ Http::post('/v1/account/tokens/magic-url') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1866,7 +1850,7 @@ Http::post('/v1/account/tokens/magic-url') ]); $user->removeAttribute('$internalId'); - $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_MAGIC_URL); @@ -1883,7 +1867,7 @@ Http::post('/v1/account/tokens/magic-url') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2015,7 +1999,7 @@ Http::post('/v1/account/tokens/magic-url') ->dynamic($token, Response::MODEL_TOKEN); }); -Http::post('/v1/account/tokens/email') +App::post('/v1/account/tokens/email') ->desc('Create email token (OTP)') ->groups(['api', 'account', 'auth']) ->label('scope', 'sessions.write') @@ -2043,8 +2027,7 @@ Http::post('/v1/account/tokens/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2053,12 +2036,12 @@ Http::post('/v1/account/tokens/email') $phrase = Phrase::generate(); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('email', [$email])]); - if (!$result->isEmpty()) { + if ($result !== false && !$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2075,7 +2058,7 @@ Http::post('/v1/account/tokens/email') $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ } @@ -2106,7 +2089,7 @@ Http::post('/v1/account/tokens/email') ]); $user->removeAttribute('$internalId'); - $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); } $tokenSecret = Auth::codeGenerator(6); @@ -2123,7 +2106,7 @@ Http::post('/v1/account/tokens/email') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2245,7 +2228,7 @@ Http::post('/v1/account/tokens/email') ->dynamic($token, Response::MODEL_TOKEN); }); -Http::put('/v1/account/sessions/magic-url') +App::put('/v1/account/sessions/magic-url') ->desc('Update magic URL session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2274,11 +2257,9 @@ Http::put('/v1/account/sessions/magic-url') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->inject('authentication') ->action($createSession); -Http::put('/v1/account/sessions/phone') +App::put('/v1/account/sessions/phone') ->desc('Update phone session') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account', 'session']) @@ -2307,11 +2288,9 @@ Http::put('/v1/account/sessions/phone') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->inject('authentication') ->action($createSession); -Http::post('/v1/account/tokens/phone') +App::post('/v1/account/tokens/phone') ->alias('/v1/account/sessions/phone') ->desc('Create phone token') ->groups(['api', 'account']) @@ -2339,18 +2318,17 @@ Http::post('/v1/account/tokens/phone') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('locale') - ->inject('authorization') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, Authorization $authorization) { + ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $result = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if (!$result->isEmpty()) { + if ($result !== false && !$result->isEmpty()) { $user->setAttributes($result->getArrayCopy()); } else { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -2389,9 +2367,9 @@ Http::post('/v1/account/tokens/phone') ]); $user->removeAttribute('$internalId'); - $user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user)); + Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); try { - $target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([ + $target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([ '$permissions' => [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), @@ -2437,7 +2415,7 @@ Http::post('/v1/account/tokens/phone') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $token = $dbForProject->createDocument('tokens', $token ->setAttribute('$permissions', [ @@ -2493,7 +2471,7 @@ Http::post('/v1/account/tokens/phone') ->dynamic($token, Response::MODEL_TOKEN); }); -Http::post('/v1/account/jwts') +App::post('/v1/account/jwts') ->alias('/v1/account/jwt') ->desc('Create JWT') ->groups(['api', 'account', 'auth']) @@ -2511,15 +2489,14 @@ Http::post('/v1/account/jwts') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('authentication') - ->action(function (Response $response, Document $user, Database $dbForProject, Authentication $authentication) { + ->action(function (Response $response, Document $user, Database $dbForProject) { $sessions = $user->getAttribute('sessions', []); $current = new Document(); foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; } } @@ -2538,7 +2515,7 @@ Http::post('/v1/account/jwts') ])]), Response::MODEL_JWT); }); -Http::get('/v1/account/prefs') +App::get('/v1/account/prefs') ->desc('Get account preferences') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2560,7 +2537,7 @@ Http::get('/v1/account/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -Http::get('/v1/account/logs') +App::get('/v1/account/logs') ->desc('List logs') ->groups(['api', 'account']) ->label('scope', 'account') @@ -2577,8 +2554,7 @@ Http::get('/v1/account/logs') ->inject('locale') ->inject('geodb') ->inject('dbForProject') - ->inject('authorization') - ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Authorization $authorization) { + ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) { try { $queries = Query::parseQueries($queries); @@ -2590,7 +2566,7 @@ Http::get('/v1/account/logs') $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; $offset = $grouped['offset'] ?? 0; - $audit = new EventAudit($dbForProject, $authorization); + $audit = new EventAudit($dbForProject); $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); @@ -2626,101 +2602,7 @@ Http::get('/v1/account/logs') ]), Response::MODEL_LOG_LIST); }); -Http::patch('/v1/account/email') - ->desc('Update email') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.email') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'updateEmail') - ->label('sdk.description', '/docs/references/account/update-email.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) - ->label('sdk.offline.model', '/account') - ->label('sdk.offline.key', 'current') - ->param('email', '', new Email(), 'User email.') - ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') - ->inject('requestTimestamp') - ->inject('response') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('project') - ->inject('hooks') - ->inject('authorization') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { - // passwordUpdate will be empty if the user has never set a password - $passwordUpdate = $user->getAttribute('passwordUpdate'); - - if ( - !empty($passwordUpdate) && - !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) - ) { // Double check user password - throw new Exception(Exception::USER_INVALID_CREDENTIALS); - } - - $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - - $oldEmail = $user->getAttribute('email'); - - $email = \strtolower($email); - - // Makes sure this email is not already used in another identity - $identityWithMatchingEmail = $dbForProject->findOne('identities', [ - Query::equal('providerEmail', [$email]), - Query::notEqual('userInternalId', $user->getInternalId()), - ]); - if (!$identityWithMatchingEmail->isEmpty()) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $user - ->setAttribute('email', $email) - ->setAttribute('emailVerification', false) // After this user needs to confirm mail again - ; - - if (empty($passwordUpdate)) { - $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) - ->setAttribute('passwordUpdate', DateTime::now()); - } - - $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ - Query::equal('identifier', [$email]), - ])); - - if (!$target->isEmpty()) { - throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); - } - - try { - $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); - /** - * @var Document $oldTarget - */ - $oldTarget = $user->find('identifier', $oldEmail, 'targets'); - - if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); - } - $dbForProject->purgeCachedDocument('users', $user->getId()); - } catch (Duplicate) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($user, Response::MODEL_ACCOUNT); - }); - - -Http::patch('/v1/account/name') +App::patch('/v1/account/name') ->desc('Update name') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.name') @@ -2753,7 +2635,7 @@ Http::patch('/v1/account/name') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::patch('/v1/account/password') +App::patch('/v1/account/password') ->desc('Update password') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.password') @@ -2823,7 +2705,99 @@ Http::patch('/v1/account/password') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::patch('/v1/account/phone') +App::patch('/v1/account/email') + ->desc('Update email') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.email') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updateEmail') + ->label('sdk.description', '/docs/references/account/update-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.offline.model', '/account') + ->label('sdk.offline.key', 'current') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->inject('requestTimestamp') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('project') + ->inject('hooks') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + // passwordUpdate will be empty if the user has never set a password + $passwordUpdate = $user->getAttribute('passwordUpdate'); + + if ( + !empty($passwordUpdate) && + !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) + ) { // Double check user password + throw new Exception(Exception::USER_INVALID_CREDENTIALS); + } + + $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); + + $oldEmail = $user->getAttribute('email'); + + $email = \strtolower($email); + + // Makes sure this email is not already used in another identity + $identityWithMatchingEmail = $dbForProject->findOne('identities', [ + Query::equal('providerEmail', [$email]), + Query::notEqual('userInternalId', $user->getInternalId()), + ]); + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $user + ->setAttribute('email', $email) + ->setAttribute('emailVerification', false) // After this user needs to confirm mail again + ; + + if (empty($passwordUpdate)) { + $user + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('passwordUpdate', DateTime::now()); + } + + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ])); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + try { + $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + } + $dbForProject->purgeCachedDocument('users', $user->getId()); + } catch (Duplicate) { + throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ + } + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_ACCOUNT); + }); + +App::patch('/v1/account/phone') ->desc('Update phone') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.phone') @@ -2848,8 +2822,7 @@ Http::patch('/v1/account/phone') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->inject('authorization') - ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, Authorization $authorization) { + ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); @@ -2862,11 +2835,11 @@ Http::patch('/v1/account/phone') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - $target = $authorization->skip(fn () => $dbForProject->findOne('targets', [ + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ])); - if (!$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } @@ -2893,7 +2866,7 @@ Http::patch('/v1/account/phone') $oldTarget = $user->find('identifier', $oldPhone, 'targets'); if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { - $authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); } $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Duplicate $th) { @@ -2905,7 +2878,7 @@ Http::patch('/v1/account/phone') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::patch('/v1/account/prefs') +App::patch('/v1/account/prefs') ->desc('Update preferences') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.prefs') @@ -2938,7 +2911,7 @@ Http::patch('/v1/account/prefs') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::patch('/v1/account/status') +App::patch('/v1/account/status') ->desc('Update status') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.status') @@ -2958,8 +2931,7 @@ Http::patch('/v1/account/status') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authentication') - ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authentication $authentication) { + ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { $user->setAttribute('status', false); @@ -2975,14 +2947,14 @@ Http::patch('/v1/account/status') $protocol = $request->getProtocol(); $response - ->addCookie($authentication->getCookieName() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::post('/v1/account/recovery') +App::post('/v1/account/recovery') ->desc('Create password recovery') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -3009,15 +2981,14 @@ Http::post('/v1/account/recovery') ->inject('locale') ->inject('queueForMails') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } $url = htmlentities($url); - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3027,7 +2998,7 @@ Http::post('/v1/account/recovery') Query::equal('email', [$email]), ]); - if ($profile->isEmpty()) { + if (!$profile) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -3051,7 +3022,7 @@ Http::post('/v1/account/recovery') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($profile->getId())->toString()); + Authorization::setRole(Role::user($profile->getId())->toString()); $recovery = $dbForProject->createDocument('tokens', $recovery ->setAttribute('$permissions', [ @@ -3073,7 +3044,7 @@ Http::post('/v1/account/recovery') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escape: false) + ->setParam('{{body}}', $body, escapeHtml: false) ->setParam('{{hello}}', $locale->getText("emails.recovery.hello")) ->setParam('{{footer}}', $locale->getText("emails.recovery.footer")) ->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks")) @@ -3163,7 +3134,7 @@ Http::post('/v1/account/recovery') ->dynamic($recovery, Response::MODEL_TOKEN); }); -Http::put('/v1/account/recovery') +App::put('/v1/account/recovery') ->desc('Create password recovery (confirmation)') ->groups(['api', 'account']) ->label('scope', 'sessions.write') @@ -3189,8 +3160,7 @@ Http::put('/v1/account/recovery') ->inject('project') ->inject('queueForEvents') ->inject('hooks') - ->inject('authorization') - ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, Authorization $authorization) { + ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) { $profile = $dbForProject->getDocument('users', $userId); if ($profile->isEmpty()) { @@ -3204,7 +3174,7 @@ Http::put('/v1/account/recovery') throw new Exception(Exception::USER_INVALID_TOKEN); } - $authorization->addRole(Role::user($profile->getId())->toString()); + Authorization::setRole(Role::user($profile->getId())->toString()); $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); @@ -3249,7 +3219,7 @@ Http::put('/v1/account/recovery') $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); -Http::post('/v1/account/verification') +App::post('/v1/account/verification') ->desc('Create email verification') ->groups(['api', 'account']) ->label('scope', 'account') @@ -3274,8 +3244,7 @@ Http::post('/v1/account/verification') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->inject('authorization') - ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, Authorization $authorization) { + ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3286,7 +3255,7 @@ Http::post('/v1/account/verification') throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $verificationSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_VERIFICATION); @@ -3303,7 +3272,7 @@ Http::post('/v1/account/verification') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3325,7 +3294,7 @@ Http::post('/v1/account/verification') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escape: false) + ->setParam('{{body}}', $body, escapeHtml: false) ->setParam('{{hello}}', $locale->getText("emails.verification.hello")) ->setParam('{{footer}}', $locale->getText("emails.verification.footer")) ->setParam('{{thanks}}', $locale->getText("emails.verification.thanks")) @@ -3415,7 +3384,7 @@ Http::post('/v1/account/verification') ->dynamic($verification, Response::MODEL_TOKEN); }); -Http::put('/v1/account/verification') +App::put('/v1/account/verification') ->desc('Create email verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3437,10 +3406,9 @@ Http::put('/v1/account/verification') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3453,7 +3421,7 @@ Http::put('/v1/account/verification') throw new Exception(Exception::USER_INVALID_TOKEN); } - $authorization->addRole(Role::user($profile->getId())->toString()); + Authorization::setRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); @@ -3475,7 +3443,7 @@ Http::put('/v1/account/verification') $response->dynamic($verification, Response::MODEL_TOKEN); }); -Http::post('/v1/account/verification/phone') +App::post('/v1/account/verification/phone') ->desc('Create phone verification') ->groups(['api', 'account', 'auth']) ->label('scope', 'account') @@ -3500,8 +3468,7 @@ Http::post('/v1/account/verification/phone') ->inject('queueForMessaging') ->inject('project') ->inject('locale') - ->inject('authorization') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, Authorization $authorization) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -3515,7 +3482,7 @@ Http::post('/v1/account/verification/phone') throw new Exception(Exception::USER_PHONE_ALREADY_VERIFIED); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -3544,7 +3511,7 @@ Http::post('/v1/account/verification/phone') 'ip' => $request->getIP(), ]); - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $verification = $dbForProject->createDocument('tokens', $verification ->setAttribute('$permissions', [ @@ -3604,7 +3571,7 @@ Http::post('/v1/account/verification/phone') ->dynamic($verification, Response::MODEL_TOKEN); }); -Http::put('/v1/account/verification/phone') +App::put('/v1/account/verification/phone') ->desc('Update phone verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') @@ -3626,10 +3593,9 @@ Http::put('/v1/account/verification/phone') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - $profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId)); + $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); @@ -3641,7 +3607,7 @@ Http::put('/v1/account/verification/phone') throw new Exception(Exception::USER_INVALID_TOKEN); } - $authorization->addRole(Role::user($profile->getId())->toString()); + Authorization::setRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); @@ -3663,7 +3629,7 @@ Http::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); -Http::patch('/v1/account/mfa') +App::patch('/v1/account/mfa') ->desc('Update MFA') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') @@ -3716,8 +3682,8 @@ Http::patch('/v1/account/mfa') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::get('/v1/account/mfa/factors') - ->desc('List Factors') +App::get('/v1/account/mfa/factors') + ->desc('List factors') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3748,8 +3714,8 @@ Http::get('/v1/account/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -Http::post('/v1/account/mfa/authenticators/:type') - ->desc('Create Authenticator') +App::post('/v1/account/mfa/authenticators/:type') + ->desc('Create authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3820,8 +3786,8 @@ Http::post('/v1/account/mfa/authenticators/:type') $response->dynamic($model, Response::MODEL_MFA_TYPE); }); -Http::put('/v1/account/mfa/authenticators/:type') - ->desc('Verify Authenticator') +App::put('/v1/account/mfa/authenticators/:type') + ->desc('Verify authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3885,8 +3851,8 @@ Http::put('/v1/account/mfa/authenticators/:type') $response->dynamic($user, Response::MODEL_ACCOUNT); }); -Http::post('/v1/account/mfa/recovery-codes') - ->desc('Create MFA Recovery Codes') +App::post('/v1/account/mfa/recovery-codes') + ->desc('Create MFA recovery codes') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3927,8 +3893,8 @@ Http::post('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::patch('/v1/account/mfa/recovery-codes') - ->desc('Regenerate MFA Recovery Codes') +App::patch('/v1/account/mfa/recovery-codes') + ->desc('Regenerate MFA recovery codes') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].update.mfa') ->label('scope', 'account') @@ -3968,8 +3934,8 @@ Http::patch('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::get('/v1/account/mfa/recovery-codes') - ->desc('Get MFA Recovery Codes') +App::get('/v1/account/mfa/recovery-codes') + ->desc('Get MFA recovery codes') ->groups(['api', 'account', 'mfaProtected']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -3998,8 +3964,8 @@ Http::get('/v1/account/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::delete('/v1/account/mfa/authenticators/:type') - ->desc('Delete Authenticator') +App::delete('/v1/account/mfa/authenticators/:type') + ->desc('Delete authenticator') ->groups(['api', 'account', 'mfaProtected']) ->label('event', 'users.[userId].delete.mfa') ->label('scope', 'account') @@ -4036,8 +4002,8 @@ Http::delete('/v1/account/mfa/authenticators/:type') $response->noContent(); }); -Http::post('/v1/account/mfa/challenge') - ->desc('Create MFA Challenge') +App::post('/v1/account/mfa/challenge') + ->desc('Create MFA challenge') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].challenges.[challengeId].create') @@ -4224,8 +4190,8 @@ Http::post('/v1/account/mfa/challenge') $response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE); }); -Http::put('/v1/account/mfa/challenge') - ->desc('Create MFA Challenge (confirmation)') +App::put('/v1/account/mfa/challenge') + ->desc('Create MFA challenge (confirmation)') ->groups(['api', 'account', 'mfa']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -4311,7 +4277,7 @@ Http::put('/v1/account/mfa/challenge') $response->dynamic($session, Response::MODEL_SESSION); }); -Http::post('/v1/account/targets/push') +App::post('/v1/account/targets/push') ->desc('Create push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4332,14 +4298,12 @@ Http::post('/v1/account/targets/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->inject('authentication') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization, Authentication $authentication) { + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; - $provider = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); if (!$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); @@ -4350,7 +4314,7 @@ Http::post('/v1/account/targets/push') $device = $detector->getDevice(); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret()); + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); $session = $dbForProject->getDocument('sessions', $sessionId); try { @@ -4386,7 +4350,7 @@ Http::post('/v1/account/targets/push') ->dynamic($target, Response::MODEL_TARGET); }); -Http::put('/v1/account/targets/:targetId/push') +App::put('/v1/account/targets/:targetId/push') ->desc('Update push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4406,10 +4370,9 @@ Http::put('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4442,7 +4405,7 @@ Http::put('/v1/account/targets/:targetId/push') ->dynamic($target, Response::MODEL_TARGET); }); -Http::delete('/v1/account/targets/:targetId/push') +App::delete('/v1/account/targets/:targetId/push') ->desc('Delete push target') ->groups(['api', 'account']) ->label('scope', 'targets.write') @@ -4462,9 +4425,8 @@ Http::delete('/v1/account/targets/:targetId/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); + ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) { + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); @@ -4489,8 +4451,8 @@ Http::delete('/v1/account/targets/:targetId/push') $response->noContent(); }); -Http::get('/v1/account/identities') - ->desc('List Identities') +App::get('/v1/account/identities') + ->desc('List identities') ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) @@ -4545,7 +4507,7 @@ Http::get('/v1/account/identities') ]), Response::MODEL_IDENTITY_LIST); }); -Http::delete('/v1/account/identities/:identityId') +App::delete('/v1/account/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'account']) ->label('scope', 'account') diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 2b8327e898..fcff3e4179 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -5,7 +5,7 @@ use Appwrite\URL\URL as URLParse; use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; -use Utopia\CLI\Console; +use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -14,17 +14,15 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Fetch\Client; -use Utopia\Http\Http; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\HexColor; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\URL; -use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; -use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\System\System; +use Utopia\Validator\Boolean; +use Utopia\Validator\HexColor; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\URL; +use Utopia\Validator\WhiteList; $avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) { @@ -63,9 +61,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, unset($image); }; -$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger, Authorization $auth) { +$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) { try { - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); @@ -116,7 +114,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro ->setAttribute('providerRefreshToken', $refreshToken) ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - $auth->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); + Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); $dbForProject->purgeCachedDocument('users', $user->getId()); } catch (Throwable $err) { @@ -124,7 +122,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro do { $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); - $user = $auth->skip(fn () => $dbForConsole->getDocument('users', $userId)); + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); $sessions = $user->getAttribute('sessions', []); $gitHubSession = new Document(); @@ -156,42 +154,11 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro 'id' => $githubId ]; } catch (Exception $error) { - if ($logger) { - $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - $log = new Log(); - $log->setNamespace('console'); - $log->setServer(\gethostname()); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', get_class($error)); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('detailedTrace', $error->getTrace()); - - $log->setAction('avatarsGetGitHub'); - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - $responseCode = $logger->addLog($log); - Console::info('GitHub error log pushed with status code: ' . $responseCode); - } - - Console::warning("Failed: {$error->getMessage()}"); - Console::warning($error->getTraceAsString()); - return []; } }; -Http::get('/v1/avatars/credit-cards/:code') +App::get('/v1/avatars/credit-cards/:code') ->desc('Get credit card icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -211,7 +178,7 @@ Http::get('/v1/avatars/credit-cards/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response)); -Http::get('/v1/avatars/browsers/:code') +App::get('/v1/avatars/browsers/:code') ->desc('Get browser icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -231,7 +198,7 @@ Http::get('/v1/avatars/browsers/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response)); -Http::get('/v1/avatars/flags/:code') +App::get('/v1/avatars/flags/:code') ->desc('Get country flag') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -251,7 +218,7 @@ Http::get('/v1/avatars/flags/:code') ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response)); -Http::get('/v1/avatars/image') +App::get('/v1/avatars/image') ->desc('Get image from URL') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -314,7 +281,7 @@ Http::get('/v1/avatars/image') unset($image); }); -Http::get('/v1/avatars/favicon') +App::get('/v1/avatars/favicon') ->desc('Get favicon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -459,7 +426,7 @@ Http::get('/v1/avatars/favicon') unset($image); }); -Http::get('/v1/avatars/qr') +App::get('/v1/avatars/qr') ->desc('Get QR code') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -499,7 +466,7 @@ Http::get('/v1/avatars/qr') ->send($image->output('png', 9)); }); -Http::get('/v1/avatars/initials') +App::get('/v1/avatars/initials') ->desc('Get user initials') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') @@ -582,8 +549,8 @@ Http::get('/v1/avatars/initials') ->file($image->getImageBlob()); }); -Http::get('/v1/cards/cloud') - ->desc('Get Front Of Cloud Card') +App::get('/v1/cards/cloud') + ->desc('Get front Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -604,9 +571,8 @@ Http::get('/v1/cards/cloud') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('authorization') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { - $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -617,7 +583,7 @@ Http::get('/v1/cards/cloud') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; @@ -790,8 +756,8 @@ Http::get('/v1/cards/cloud') ->file($baseImage->getImageBlob()); }); -Http::get('/v1/cards/cloud-back') - ->desc('Get Back Of Cloud Card') +App::get('/v1/cards/cloud-back') + ->desc('Get back Of Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -812,9 +778,8 @@ Http::get('/v1/cards/cloud-back') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('authorization') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { - $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -824,7 +789,7 @@ Http::get('/v1/cards/cloud-back') $userId = $user->getId(); $email = $user->getAttribute('email', ''); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); $githubId = $gitHub['id'] ?? ''; $isHero = \array_key_exists($email, $heroes); @@ -869,8 +834,8 @@ Http::get('/v1/cards/cloud-back') ->file($baseImage->getImageBlob()); }); -Http::get('/v1/cards/cloud-og') - ->desc('Get OG Image From Cloud Card') +App::get('/v1/cards/cloud-og') + ->desc('Get OG image From Cloud Card') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('cache', true) @@ -891,9 +856,8 @@ Http::get('/v1/cards/cloud-og') ->inject('contributors') ->inject('employees') ->inject('logger') - ->inject('authorization') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) { - $user = $authorization->skip(fn () => $dbForConsole->getDocument('users', $userId)); + ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { + $user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId)); if ($user->isEmpty() && empty($mock)) { throw new Exception(Exception::USER_NOT_FOUND); @@ -908,7 +872,7 @@ Http::get('/v1/cards/cloud-og') $email = $user->getAttribute('email', ''); $createdAt = new \DateTime($user->getCreatedAt()); - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger, $authorization); + $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger); $githubName = $gitHub['name'] ?? ''; $githubId = $gitHub['id'] ?? ''; diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index 2a393497ae..eeb823a3d3 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -2,12 +2,12 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Database\Document; -use Utopia\Http\Http; -use Utopia\Http\Validator\Text; use Utopia\System\System; +use Utopia\Validator\Text; -Http::init() +App::init() ->groups(['console']) ->inject('project') ->action(function (Document $project) { @@ -17,7 +17,7 @@ Http::init() }); -Http::get('/v1/console/variables') +App::get('/v1/console/variables') ->desc('Get variables') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -56,8 +56,8 @@ Http::get('/v1/console/variables') $response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES); }); -Http::post('/v1/console/assistant') - ->desc('Ask Query') +App::post('/v1/console/assistant') + ->desc('Ask query') ->groups(['api', 'assistant']) ->label('scope', 'assistant.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 98aace214b..9c110647af 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -16,6 +16,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Indexes; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -34,7 +35,6 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Key; @@ -44,19 +44,18 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\FloatValidator; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\IP; -use Utopia\Http\Validator\JSON; -use Utopia\Http\Validator\Nullable; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\URL; -use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Boolean; +use Utopia\Validator\FloatValidator; +use Utopia\Validator\Integer; +use Utopia\Validator\IP; +use Utopia\Validator\JSON; +use Utopia\Validator\Nullable; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\URL; +use Utopia\Validator\WhiteList; /** * * Create attribute of varying type @@ -78,7 +77,7 @@ use Utopia\Locale\Locale; * @throws ConflictException * @throws Exception */ -function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document +function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document { $key = $attribute->getAttribute('key'); $type = $attribute->getAttribute('type', ''); @@ -92,7 +91,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); - $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -227,7 +226,6 @@ function createAttribute(string $databaseId, string $collectionId, Document $att } function updateAttribute( - Authorization $authorization, string $databaseId, string $collectionId, string $key, @@ -241,10 +239,10 @@ function updateAttribute( int|float $min = null, int|float $max = null, array $elements = null, - string $newKey = null, array $options = [], + string $newKey = null, ): Document { - $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -423,19 +421,19 @@ function updateAttribute( return $attribute; } -Http::init() +App::init() ->groups(['api', 'database']) ->inject('request') ->inject('dbForProject') ->action(function (Request $request, Database $dbForProject) { $timeout = \intval($request->getHeader('x-appwrite-timeout')); - if (!empty($timeout) && Http::isDevelopment()) { + if (!empty($timeout) && App::isDevelopment()) { $dbForProject->setTimeout($timeout); } }); -Http::post('/v1/databases') +App::post('/v1/databases') ->desc('Create database') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].create') @@ -513,7 +511,7 @@ Http::post('/v1/databases') ->dynamic($database, Response::MODEL_DATABASE); }); -Http::get('/v1/databases') +App::get('/v1/databases') ->desc('List databases') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -566,7 +564,7 @@ Http::get('/v1/databases') ]), Response::MODEL_DATABASE_LIST); }); -Http::get('/v1/databases/:databaseId') +App::get('/v1/databases/:databaseId') ->desc('Get database') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -591,7 +589,7 @@ Http::get('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -Http::get('/v1/databases/:databaseId/logs') +App::get('/v1/databases/:databaseId/logs') ->desc('List database logs') ->groups(['api', 'database']) ->label('scope', 'databases.read') @@ -608,8 +606,7 @@ Http::get('/v1/databases/:databaseId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -683,7 +680,7 @@ Http::get('/v1/databases/:databaseId/logs') }); -Http::put('/v1/databases/:databaseId') +App::put('/v1/databases/:databaseId') ->desc('Update database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -721,7 +718,7 @@ Http::put('/v1/databases/:databaseId') $response->dynamic($database, Response::MODEL_DATABASE); }); -Http::delete('/v1/databases/:databaseId') +App::delete('/v1/databases/:databaseId') ->desc('Delete database') ->groups(['api', 'database', 'schema']) ->label('scope', 'databases.write') @@ -769,7 +766,7 @@ Http::delete('/v1/databases/:databaseId') $response->noContent(); }); -Http::post('/v1/databases/:databaseId/collections') +App::post('/v1/databases/:databaseId/collections') ->desc('Create collection') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].create') @@ -793,10 +790,9 @@ Http::post('/v1/databases/:databaseId/collections') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -837,7 +833,7 @@ Http::post('/v1/databases/:databaseId/collections') ->dynamic($collection, Response::MODEL_COLLECTION); }); -Http::get('/v1/databases/:databaseId/collections') +App::get('/v1/databases/:databaseId/collections') ->alias('/v1/database/collections', ['databaseId' => 'default']) ->desc('List collections') ->groups(['api', 'database']) @@ -855,10 +851,9 @@ Http::get('/v1/databases/:databaseId/collections') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -901,7 +896,7 @@ Http::get('/v1/databases/:databaseId/collections') ]), Response::MODEL_COLLECTION_LIST); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId') +App::get('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Get collection') ->groups(['api', 'database']) @@ -918,10 +913,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -936,7 +930,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/logs') +App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->alias('/v1/database/collections/:collectionId/logs', ['databaseId' => 'default']) ->desc('List collection logs') ->groups(['api', 'database']) @@ -955,10 +949,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1037,7 +1030,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/logs') }); -Http::put('/v1/databases/:databaseId/collections/:collectionId') +App::put('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Update collection') ->groups(['api', 'database', 'schema']) @@ -1062,10 +1055,9 @@ Http::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('mode') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1101,7 +1093,7 @@ Http::put('/v1/databases/:databaseId/collections/:collectionId') $response->dynamic($collection, Response::MODEL_COLLECTION); }); -Http::delete('/v1/databases/:databaseId/collections/:collectionId') +App::delete('/v1/databases/:databaseId/collections/:collectionId') ->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default']) ->desc('Delete collection') ->groups(['api', 'database', 'schema']) @@ -1122,10 +1114,9 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1157,7 +1148,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId') $response->noContent(); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string') ->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default']) ->desc('Create string attribute') ->groups(['api', 'database', 'schema']) @@ -1184,8 +1175,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { // Ensure attribute default is within required size $validator = new Text($size, 0); @@ -1207,7 +1197,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/strin 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response @@ -1215,7 +1205,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email') ->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default']) ->desc('Create email attribute') ->groups(['api', 'database', 'schema']) @@ -1240,8 +1230,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1251,14 +1240,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default']) ->desc('Create enum attribute') ->groups(['api', 'database', 'schema']) @@ -1284,8 +1273,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum' ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { if (!is_null($default) && !in_array($default, $elements)) { throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Default value not found in elements'); } @@ -1299,14 +1287,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum' 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default']) ->desc('Create IP address attribute') ->groups(['api', 'database', 'schema']) @@ -1331,8 +1319,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1342,14 +1329,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_IP, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default']) ->desc('Create URL attribute') ->groups(['api', 'database', 'schema']) @@ -1374,8 +1361,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1385,14 +1371,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer') ->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default']) ->desc('Create integer attribute') ->groups(['api', 'database', 'schema']) @@ -1419,8 +1405,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { // Ensure attribute default is within range $min = (is_null($min)) ? PHP_INT_MIN : \intval($min); @@ -1450,7 +1435,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integ 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1464,7 +1449,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float') ->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default']) ->desc('Create float attribute') ->groups(['api', 'database', 'schema']) @@ -1491,8 +1476,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { // Ensure attribute default is within range $min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min); @@ -1525,7 +1509,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1539,7 +1523,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean') ->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default']) ->desc('Create boolean attribute') ->groups(['api', 'database', 'schema']) @@ -1564,8 +1548,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boole ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1574,14 +1557,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boole 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime') ->alias('/v1/database/collections/:collectionId/attributes/datetime', ['databaseId' => 'default']) ->desc('Create datetime attribute') ->groups(['api', 'database']) @@ -1606,8 +1589,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datet ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $filters[] = 'datetime'; @@ -1619,14 +1601,14 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datet 'default' => $default, 'array' => $array, 'filters' => $filters, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') +App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship') ->alias('/v1/database/collections/:collectionId/attributes/relationship', ['databaseId' => 'default']) ->desc('Create relationship attribute') ->groups(['api', 'database']) @@ -1653,7 +1635,6 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relat ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -1666,13 +1647,12 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relat Response $response, Database $dbForProject, EventDatabase $queueForDatabase, - Event $queueForEvents, - Authorization $authorization + Event $queueForEvents ) { $key ??= $relatedCollectionId; $twoWayKey ??= $collectionId; - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1742,8 +1722,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relat $response, $dbForProject, $queueForDatabase, - $queueForEvents, - $authorization + $queueForEvents ); $options = $attribute->getAttribute('options', []); @@ -1757,7 +1736,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relat ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') +App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->alias('/v1/database/collections/:collectionId/attributes', ['databaseId' => 'default']) ->desc('List attributes') ->groups(['api', 'database']) @@ -1774,10 +1753,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->param('queries', [], new Attributes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Attributes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { /** @var Document $database */ - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1811,7 +1789,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') if ($cursor) { $attributeId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->find('attributes', [ + $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$attributeId]), @@ -1836,7 +1814,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ]), Response::MODEL_ATTRIBUTE_LIST); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Get attribute') ->groups(['api', 'database']) @@ -1863,10 +1841,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1912,7 +1889,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') $response->dynamic($attribute, $model); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key') ->desc('Update string attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1935,11 +1912,9 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/stri ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -1957,7 +1932,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/stri ->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key') ->desc('Update email attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -1979,10 +1954,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/emai ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2000,7 +1973,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/emai ->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key') ->desc('Update enum attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2023,10 +1996,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2045,7 +2016,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum ->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key') ->desc('Update IP address attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2067,10 +2038,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/: ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2088,7 +2057,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/: ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key') ->desc('Update URL attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2110,10 +2079,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/ ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2131,7 +2098,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/ ->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key') ->desc('Update integer attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2155,10 +2122,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/inte ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2184,7 +2149,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/inte ->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key') ->desc('Update float attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2208,10 +2173,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/floa ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2237,7 +2200,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/floa ->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key') ->desc('Update boolean attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2259,10 +2222,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/bool ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2279,7 +2240,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/bool ->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key') ->desc('Update dateTime attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2301,10 +2262,8 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/date ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { $attribute = updateAttribute( - authorization: $authorization, databaseId: $databaseId, collectionId: $collectionId, key: $key, @@ -2321,7 +2280,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/date ->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') +App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship') ->desc('Update relationship attribute') ->groups(['api', 'database', 'schema']) ->label('scope', 'collections.write') @@ -2342,7 +2301,6 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') ->action(function ( string $databaseId, string $collectionId, @@ -2351,11 +2309,9 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ?string $newKey, Response $response, Database $dbForProject, - Event $queueForEvents, - Authorization $authorization + Event $queueForEvents ) { $attribute = updateAttribute( - $authorization, $databaseId, $collectionId, $key, @@ -2380,7 +2336,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->dynamic($attribute, Response::MODEL_ATTRIBUTE_RELATIONSHIP); }); -Http::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') +App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') ->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default']) ->desc('Delete attribute') ->groups(['api', 'database', 'schema']) @@ -2402,10 +2358,9 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:ke ->inject('queueForDatabase') ->inject('queueForEvents') ->inject('queueForUsage') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) { - $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2494,7 +2449,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:ke $response->noContent(); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/indexes') +App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('Create index') ->groups(['api', 'database']) @@ -2519,10 +2474,9 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2665,7 +2619,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->dynamic($index, Response::MODEL_INDEX); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') +App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default']) ->desc('List indexes') ->groups(['api', 'database']) @@ -2682,10 +2636,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->param('queries', [], new Indexes(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Indexes::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { /** @var Document $database */ - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2715,7 +2668,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') if ($cursor) { $indexId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->find('indexes', [ + $cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), Query::equal('databaseInternalId', [$database->getInternalId()]), Query::equal('key', [$indexId]), @@ -2736,7 +2689,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ]), Response::MODEL_INDEX_LIST); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Get index') ->groups(['api', 'database']) @@ -2753,10 +2706,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2776,7 +2728,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') }); -Http::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') +App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) ->desc('Delete index') ->groups(['api', 'database']) @@ -2797,10 +2749,9 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2841,7 +2792,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $response->noContent(); }); -Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') +App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('Create document') ->groups(['api', 'database']) @@ -2872,8 +2823,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2885,16 +2835,16 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead'); } - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -2932,8 +2882,8 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!$authorization->isRole($role)) { - throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')'); + if (!Authorization::isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')'); } } } @@ -2944,16 +2894,17 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') $data['$permissions'] = $permissions; $document = new Document($data); - $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, $authorization) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) { $documentSecurity = $collection->getAttribute('documentSecurity', false); + $validator = new Authorization($permission); - $valid = $authorization->isValid(new Input($permission, $collection->getPermissionsByType($permission))); + $valid = $validator->isValid($collection->getPermissionsByType($permission)); if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } if ($permission === Database::PERMISSION_UPDATE) { - $valid = $valid || $authorization->isValid($document->getUpdate()); + $valid = $valid || $validator->isValid($document->getUpdate()); if ($documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -2980,7 +2931,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -2994,7 +2945,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') $relation = new Document($relation); } if ($relation instanceof Document) { - $current = $authorization->skip( + $current = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId()) ); @@ -3034,7 +2985,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3054,7 +3005,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3093,7 +3044,7 @@ Http::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') +App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) ->desc('List documents') ->groups(['api', 'database']) @@ -3112,17 +3063,16 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3146,7 +3096,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') if ($cursor) { $documentId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$documentId}' for the 'cursor' value not found."); @@ -3165,7 +3115,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') } // Add $collectionId and $databaseId for all documents - $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization): bool { + $processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool { if ($document->isEmpty()) { return false; } @@ -3192,7 +3142,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); + $relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId)); foreach ($relations as $index => $doc) { if ($doc instanceof Document) { @@ -3249,7 +3199,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents') ]), Response::MODEL_DOCUMENT_LIST); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Get document') ->groups(['api', 'database']) @@ -3270,18 +3220,17 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3301,7 +3250,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { if ($document->isEmpty()) { return; } @@ -3325,7 +3274,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3342,7 +3291,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume $response->dynamic($document, Response::MODEL_DOCUMENT); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') +App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) ->desc('List document logs') ->groups(['api', 'database']) @@ -3362,10 +3311,9 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3447,7 +3395,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/documents/:docume ]), Response::MODEL_LOG_LIST); }); -Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Update document') ->groups(['api', 'database']) @@ -3477,8 +3425,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('dbForProject') ->inject('queueForEvents') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Authorization $authorization) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -3486,16 +3433,16 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -3503,7 +3450,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu // Read permission should not be required for update /** @var Document $document */ - $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3517,7 +3464,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -3530,7 +3477,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!$authorization->isRole($role)) { + if (!Authorization::isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -3545,7 +3492,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $data['$permissions'] = $permissions; $newDocument = new Document($data); - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, $authorization) { + $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) { $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP @@ -3567,7 +3514,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3582,7 +3529,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $relation = new Document($relation); } if ($relation instanceof Document) { - $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument( + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId() )); @@ -3631,7 +3578,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3651,7 +3598,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3684,7 +3631,7 @@ Http::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->setPayload($response->getPayload(), sensitive: $relationships); }); -Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') +App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) ->desc('Delete document') ->groups(['api', 'database']) @@ -3713,25 +3660,24 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:doc ->inject('queueForEvents') ->inject('queueForUsage') ->inject('mode') - ->inject('authorization') - ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode, Authorization $authorization) { - $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Usage $queueForUsage, string $mode) { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); + $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Read permission should not be required for delete - $document = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); @@ -3745,7 +3691,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:doc }); // Add $collectionId and $databaseId for all documents - $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, $authorization) { + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { $document->setAttribute('$databaseId', $database->getId()); $document->setAttribute('$collectionId', $collection->getId()); @@ -3765,7 +3711,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:doc } $relatedCollectionId = $relationship->getAttribute('relatedCollection'); - $relatedCollection = $authorization->skip( + $relatedCollection = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) ); @@ -3805,7 +3751,7 @@ Http::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:doc $response->noContent(); }); -Http::get('/v1/databases/usage') +App::get('/v1/databases/usage') ->desc('Get databases usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3818,8 +3764,7 @@ Http::get('/v1/databases/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $range, Response $response, Database $dbForProject) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -3831,7 +3776,7 @@ Http::get('/v1/databases/usage') METRIC_DATABASES_STORAGE ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3887,7 +3832,7 @@ Http::get('/v1/databases/usage') ]), Response::MODEL_USAGE_DATABASES); }); -Http::get('/v1/databases/:databaseId/usage') +App::get('/v1/databases/:databaseId/usage') ->desc('Get database usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') @@ -3901,8 +3846,7 @@ Http::get('/v1/databases/:databaseId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -3919,7 +3863,7 @@ Http::get('/v1/databases/:databaseId/usage') str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_STORAGE) ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -3974,7 +3918,7 @@ Http::get('/v1/databases/:databaseId/usage') ]), Response::MODEL_USAGE_DATABASE); }); -Http::get('/v1/databases/:databaseId/collections/:collectionId/usage') +App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) ->desc('Get collection usage stats') ->groups(['api', 'database', 'usage']) @@ -3990,8 +3934,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $databaseId, string $range, string $collectionId, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); @@ -4008,7 +3951,7 @@ Http::get('/v1/databases/:databaseId/collections/:collectionId/usage') str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collectionDocument->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 77311c8a90..9c3e6782b4 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -2,7 +2,6 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; -use Appwrite\Auth\Authentication; use Appwrite\Event\Build; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -21,11 +20,11 @@ use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Executions; use Appwrite\Utopia\Database\Validator\Queries\Functions; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Rule; use Executor\Executor; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -38,25 +37,24 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\AnyOf; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Assoc; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Nullable; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Storage\Device; use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; +use Utopia\Swoole\Request; use Utopia\System\System; +use Utopia\Validator\AnyOf; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Assoc; +use Utopia\Validator\Boolean; +use Utopia\Validator\Nullable; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -135,7 +133,7 @@ $redeployVcs = function (Request $request, Document $function, Document $project ->setTemplate($template); }; -Http::post('/v1/functions') +App::post('/v1/functions') ->groups(['api', 'functions']) ->desc('Create function') ->label('scope', 'functions.write') @@ -173,8 +171,8 @@ Http::post('/v1/functions') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -185,8 +183,7 @@ Http::post('/v1/functions') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->inject('authorization') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); @@ -250,7 +247,7 @@ Http::post('/v1/functions') 'specification' => $specification ])); - $schedule = $authorization->skip( + $schedule = Authorization::skip( fn () => $dbForConsole->createDocument('schedules', new Document([ 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region 'resourceType' => 'function', @@ -332,7 +329,7 @@ Http::post('/v1/functions') $routeSubdomain = ID::unique(); $domain = "{$routeSubdomain}.{$functionsDomain}"; - $rule = $authorization->skip( + $rule = Authorization::skip( fn () => $dbForConsole->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -399,7 +396,7 @@ Http::post('/v1/functions') ->dynamic($function, Response::MODEL_FUNCTION); }); -Http::get('/v1/functions') +App::get('/v1/functions') ->groups(['api', 'functions']) ->desc('List functions') ->label('scope', 'functions.read') @@ -453,7 +450,7 @@ Http::get('/v1/functions') ]), Response::MODEL_FUNCTION_LIST); }); -Http::get('/v1/functions/runtimes') +App::get('/v1/functions/runtimes') ->groups(['api', 'functions']) ->desc('List runtimes') ->label('scope', 'functions.read') @@ -486,7 +483,7 @@ Http::get('/v1/functions/runtimes') ]), Response::MODEL_RUNTIME_LIST); }); -Http::get('/v1/functions/specifications') +App::get('/v1/functions/specifications') ->groups(['api', 'functions']) ->desc('List available function runtime specifications') ->label('scope', 'functions.read') @@ -522,7 +519,7 @@ Http::get('/v1/functions/specifications') ]), Response::MODEL_SPECIFICATION_LIST); }); -Http::get('/v1/functions/:functionId') +App::get('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Get function') ->label('scope', 'functions.read') @@ -546,7 +543,7 @@ Http::get('/v1/functions/:functionId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -Http::get('/v1/functions/:functionId/usage') +App::get('/v1/functions/:functionId/usage') ->desc('Get function usage') ->groups(['api', 'functions', 'usage']) ->label('scope', 'functions.read') @@ -560,8 +557,7 @@ Http::get('/v1/functions/:functionId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $functionId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); @@ -584,7 +580,7 @@ Http::get('/v1/functions/:functionId/usage') str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -651,7 +647,7 @@ Http::get('/v1/functions/:functionId/usage') ]), Response::MODEL_USAGE_FUNCTION); }); -Http::get('/v1/functions/usage') +App::get('/v1/functions/usage') ->desc('Get functions usage') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -664,8 +660,7 @@ Http::get('/v1/functions/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $range, Response $response, Database $dbForProject) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -683,7 +678,7 @@ Http::get('/v1/functions/usage') METRIC_EXECUTIONS_MB_SECONDS, ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -751,7 +746,7 @@ Http::get('/v1/functions/usage') ]), Response::MODEL_USAGE_FUNCTIONS); }); -Http::put('/v1/functions/:functionId') +App::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update function') ->label('scope', 'functions.write') @@ -785,8 +780,8 @@ Http::put('/v1/functions/:functionId') ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), - System::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - System::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) ->inject('request') ->inject('response') @@ -796,8 +791,7 @@ Http::put('/v1/functions/:functionId') ->inject('queueForBuilds') ->inject('dbForConsole') ->inject('gitHub') - ->inject('authorization') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github, Authorization $authorization) use ($redeployVcs) { + ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { // TODO: If only branch changes, re-deploy $function = $dbForProject->getDocument('functions', $functionId); @@ -900,7 +894,7 @@ Http::put('/v1/functions/:functionId') // Enforce Cold Start if spec limits change. if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); try { $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); } catch (\Throwable $th) { @@ -947,14 +941,14 @@ Http::put('/v1/functions/:functionId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents->setParam('functionId', $function->getId()); $response->dynamic($function, Response::MODEL_FUNCTION); }); -Http::get('/v1/functions/:functionId/deployments/:deploymentId/download') +App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->groups(['api', 'functions']) ->desc('Download deployment') ->label('scope', 'functions.read') @@ -1039,7 +1033,7 @@ Http::get('/v1/functions/:functionId/deployments/:deploymentId/download') } }); -Http::patch('/v1/functions/:functionId/deployments/:deploymentId') +App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Update deployment') ->label('scope', 'functions.write') @@ -1059,8 +1053,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId') ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForConsole') - ->inject('authorization') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -1093,7 +1086,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents ->setParam('functionId', $function->getId()) @@ -1102,7 +1095,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($function, Response::MODEL_FUNCTION); }); -Http::delete('/v1/functions/:functionId') +App::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete function') ->label('scope', 'functions.write') @@ -1121,8 +1114,7 @@ Http::delete('/v1/functions/:functionId') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('dbForConsole') - ->inject('authorization') - ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole, Authorization $authorization) { + ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); @@ -1139,7 +1131,7 @@ Http::delete('/v1/functions/:functionId') $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) @@ -1150,7 +1142,7 @@ Http::delete('/v1/functions/:functionId') $response->noContent(); }); -Http::post('/v1/functions/:functionId/deployments') +App::post('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('Create deployment') ->label('scope', 'functions.write') @@ -1369,7 +1361,7 @@ Http::post('/v1/functions/:functionId/deployments') ->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -Http::get('/v1/functions/:functionId/deployments') +App::get('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('List deployments') ->label('scope', 'functions.read') @@ -1446,7 +1438,7 @@ Http::get('/v1/functions/:functionId/deployments') ]), Response::MODEL_DEPLOYMENT_LIST); }); -Http::get('/v1/functions/:functionId/deployments/:deploymentId') +App::get('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Get deployment') ->label('scope', 'functions.read') @@ -1489,7 +1481,7 @@ Http::get('/v1/functions/:functionId/deployments/:deploymentId') $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); -Http::delete('/v1/functions/:functionId/deployments/:deploymentId') +App::delete('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Delete deployment') ->label('scope', 'functions.write') @@ -1553,7 +1545,7 @@ Http::delete('/v1/functions/:functionId/deployments/:deploymentId') $response->noContent(); }); -Http::post('/v1/functions/:functionId/deployments/:deploymentId/build') +App::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->groups(['api', 'functions']) ->desc('Rebuild deployment') @@ -1576,8 +1568,7 @@ Http::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForFunctions') - ->inject('authorization') - ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions, Authorization $authorization) { + ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1623,7 +1614,7 @@ Http::post('/v1/functions/:functionId/deployments/:deploymentId/build') $response->noContent(); }); -Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') +App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->groups(['api', 'functions']) ->desc('Cancel deployment') ->label('scope', 'functions.write') @@ -1641,8 +1632,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1655,7 +1645,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { $buildId = ID::unique(); @@ -1697,7 +1687,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); try { - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist @@ -1713,7 +1703,7 @@ Http::patch('/v1/functions/:functionId/deployments/:deploymentId/build') $response->dynamic($build, Response::MODEL_BUILD); }); -Http::post('/v1/functions/:functionId/executions') +App::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create execution') ->label('scope', 'execution.write') @@ -1743,9 +1733,7 @@ Http::post('/v1/functions/:functionId/executions') ->inject('queueForUsage') ->inject('queueForFunctions') ->inject('geodb') - ->inject('authorization') - ->inject('authentication') - ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, Authorization $authorization, Authentication $authentication) { + ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) { $async = \strval($async) === 'true' || \strval($async) === '1'; if (!$async && !is_null($scheduledAt)) { @@ -1775,10 +1763,10 @@ Http::post('/v1/functions/:functionId/executions') throw new Exception($validator->getDescription(), 400); } - $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -1794,7 +1782,7 @@ Http::post('/v1/functions/:functionId/executions') throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -1805,7 +1793,7 @@ Http::post('/v1/functions/:functionId/executions') } /** Check if build has completed */ - $build = $authorization->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new Exception(Exception::BUILD_NOT_FOUND); } @@ -1814,8 +1802,10 @@ Http::post('/v1/functions/:functionId/executions') throw new Exception(Exception::BUILD_NOT_READY); } - if (!$authorization->isValid(new Input('execute', $function->getAttribute('execute')))) { // Check if user has write access to execute function - throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); + $validator = new Authorization('execute'); + + if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function + throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); } $jwt = ''; // initialize @@ -1825,7 +1815,7 @@ Http::post('/v1/functions/:functionId/executions') foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash($authentication->getSecret())) { // If current session delete the cookies too + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; } } @@ -1909,9 +1899,8 @@ Http::post('/v1/functions/:functionId/executions') ->setContext('function', $function); if ($async) { - if (is_null($scheduledAt)) { - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); $queueForFunctions ->setType('http') ->setExecution($execution) @@ -1952,7 +1941,7 @@ Http::post('/v1/functions/:functionId/executions') ->setAttribute('scheduleInternalId', $schedule->getInternalId()) ->setAttribute('scheduledAt', $scheduledAt); - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response @@ -2038,7 +2027,8 @@ Http::post('/v1/functions/:functionId/executions') runtimeEntrypoint: $command, cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true) + logging: $function->getAttribute('logging', true), + requestTimeout: 30 ); $headersFiltered = []; @@ -2079,10 +2069,10 @@ Http::post('/v1/functions/:functionId/executions') ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT))) ; - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2115,7 +2105,7 @@ Http::post('/v1/functions/:functionId/executions') ->dynamic($execution, Response::MODEL_EXECUTION); }); -Http::get('/v1/functions/:functionId/executions') +App::get('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('List executions') ->label('scope', 'execution.read') @@ -2132,12 +2122,11 @@ Http::get('/v1/functions/:functionId/executions') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2180,7 +2169,7 @@ Http::get('/v1/functions/:functionId/executions') $results = $dbForProject->find('executions', $queries); $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2197,7 +2186,7 @@ Http::get('/v1/functions/:functionId/executions') ]), Response::MODEL_EXECUTION_LIST); }); -Http::get('/v1/functions/:functionId/executions/:executionId') +App::get('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Get execution') ->label('scope', 'execution.read') @@ -2213,12 +2202,11 @@ Http::get('/v1/functions/:functionId/executions/:executionId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -2234,7 +2222,7 @@ Http::get('/v1/functions/:functionId/executions/:executionId') throw new Exception(Exception::EXECUTION_NOT_FOUND); } - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { @@ -2245,7 +2233,7 @@ Http::get('/v1/functions/:functionId/executions/:executionId') $response->dynamic($execution, Response::MODEL_EXECUTION); }); -Http::delete('/v1/functions/:functionId/executions/:executionId') +App::delete('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Delete execution') ->label('scope', 'execution.write') @@ -2264,8 +2252,7 @@ Http::delete('/v1/functions/:functionId/executions/:executionId') ->inject('dbForProject') ->inject('dbForConsole') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2297,12 +2284,12 @@ Http::delete('/v1/functions/:functionId/executions/:executionId') Query::equal('active', [true]), ]); - if (!$schedule->isEmpty()) { + if ($schedule && !$schedule->isEmpty()) { $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } } @@ -2316,7 +2303,7 @@ Http::delete('/v1/functions/:functionId/executions/:executionId') // Variables -Http::post('/v1/functions/:functionId/variables') +App::post('/v1/functions/:functionId/variables') ->desc('Create variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2335,8 +2322,7 @@ Http::post('/v1/functions/:functionId/variables') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->inject('authorization') - ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { + ->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2374,14 +2360,14 @@ Http::post('/v1/functions/:functionId/variables') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::get('/v1/functions/:functionId/variables') +App::get('/v1/functions/:functionId/variables') ->desc('List variables') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2408,7 +2394,7 @@ Http::get('/v1/functions/:functionId/variables') ]), Response::MODEL_VARIABLE_LIST); }); -Http::get('/v1/functions/:functionId/variables/:variableId') +App::get('/v1/functions/:functionId/variables/:variableId') ->desc('Get variable') ->groups(['api', 'functions']) ->label('scope', 'functions.read') @@ -2447,7 +2433,7 @@ Http::get('/v1/functions/:functionId/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::put('/v1/functions/:functionId/variables/:variableId') +App::put('/v1/functions/:functionId/variables/:variableId') ->desc('Update variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2467,8 +2453,7 @@ Http::put('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->inject('authorization') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { + ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); @@ -2504,12 +2489,12 @@ Http::put('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::delete('/v1/functions/:functionId/variables/:variableId') +App::delete('/v1/functions/:functionId/variables/:variableId') ->desc('Delete variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') @@ -2526,8 +2511,7 @@ Http::delete('/v1/functions/:functionId/variables/:variableId') ->inject('response') ->inject('dbForProject') ->inject('dbForConsole') - ->inject('authorization') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization) { + ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -2553,12 +2537,12 @@ Http::delete('/v1/functions/:functionId/variables/:variableId') ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $authorization->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); $response->noContent(); }); -Http::get('/v1/functions/templates') +App::get('/v1/functions/templates') ->groups(['api']) ->desc('List function templates') ->label('scope', 'public') @@ -2596,7 +2580,7 @@ Http::get('/v1/functions/templates') ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); }); -Http::get('/v1/functions/templates/:templateId') +App::get('/v1/functions/templates/:templateId') ->desc('Get function template') ->label('scope', 'public') ->label('sdk.namespace', 'functions') @@ -2611,10 +2595,9 @@ Http::get('/v1/functions/templates/:templateId') ->action(function (string $templateId, Response $response) { $templates = Config::getParam('function-templates', []); - $array = \array_filter($templates, function ($template) use ($templateId) { + $template = array_shift(\array_filter($templates, function ($template) use ($templateId) { return $template['id'] === $templateId; - }); - $template = array_shift($array); + })); if (empty($template)) { throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 0e7ddc783d..f79f433b5c 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -14,28 +14,27 @@ use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; use Swoole\Coroutine\WaitGroup; +use Utopia\App; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\Http\Http; -use Utopia\Http\Validator\JSON; -use Utopia\Http\Validator\Text; use Utopia\System\System; +use Utopia\Validator\JSON; +use Utopia\Validator\Text; -Http::init() +App::init() ->groups(['graphql']) ->inject('project') - ->inject('authorization') - ->action(function (Document $project, Authorization $authorization) { + ->action(function (Document $project) { if ( array_key_exists('graphql', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } }); -Http::get('/v1/graphql') +App::get('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -75,7 +74,7 @@ Http::get('/v1/graphql') ->json($output); }); -Http::post('/v1/graphql/mutation') +App::post('/v1/graphql/mutation') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -120,7 +119,7 @@ Http::post('/v1/graphql/mutation') ->json($output); }); -Http::post('/v1/graphql') +App::post('/v1/graphql') ->desc('GraphQL endpoint') ->groups(['graphql']) ->label('scope', 'graphql') @@ -157,6 +156,7 @@ Http::post('/v1/graphql') if (\str_starts_with($type, 'multipart/form-data')) { $query = parseMultipart($query, $request); } + $output = execute($schema, $promiseAdapter, $query); $response @@ -205,7 +205,7 @@ function execute( $validations[] = new QueryComplexity($maxComplexity); $validations[] = new QueryDepth($maxDepth); } - if (Http::getMode() === Http::MODE_TYPE_PRODUCTION) { + if (App::getMode() === App::MODE_TYPE_PRODUCTION) { $flags = DebugFlag::NONE; } @@ -306,10 +306,9 @@ function processResult($result, $debugFlags): array ); } -Http::shutdown() +App::shutdown() ->groups(['schema']) ->inject('project') - ->inject('schemaVariable') - ->action(function (Document $project, Schema $schemaVariable) { - $schemaVariable->setDirty($project->getId()); + ->action(function (Document $project) { + Schema::setDirty($project->getId()); }); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 47d80dbf71..f4581df8e4 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -3,19 +3,12 @@ use Appwrite\ClamAV\Network; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Config\Config; -use Utopia\Database\Adapter\MariaDB; -use Utopia\Database\Adapter\MySQL; use Utopia\Database\Document; use Utopia\Domains\Validator\PublicDomain; -use Utopia\Http\Http; -use Utopia\Http\Validator\Domain; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Multiple; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; +use Utopia\Pools\Group; use Utopia\Queue\Client; use Utopia\Queue\Connection; use Utopia\Registry\Registry; @@ -23,8 +16,13 @@ use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\System\System; +use Utopia\Validator\Domain; +use Utopia\Validator\Integer; +use Utopia\Validator\Multiple; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; -Http::get('/v1/health') +App::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -47,7 +45,7 @@ Http::get('/v1/health') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -Http::get('/v1/health/version') +App::get('/v1/health/version') ->desc('Get version') ->groups(['api', 'health']) ->label('scope', 'public') @@ -59,7 +57,7 @@ Http::get('/v1/health/version') $response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION); }); -Http::get('/v1/health/db') +App::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -72,34 +70,21 @@ Http::get('/v1/health/db') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->inject('connections') - ->action(function (Response $response, array $pools, Connections $connections) { + ->action(function (Response $response, Group $pools) { $output = []; $configs = [ - 'console' => Config::getParam('pools-console'), - 'database' => Config::getParam('pools-database'), + 'Console.DB' => Config::getParam('pools-console'), + 'Projects.DB' => Config::getParam('pools-database'), ]; foreach ($configs as $key => $config) { foreach ($config as $database) { - $checkStart = \microtime(true); - try { + $adapter = $pools->get($database)->pop()->getResource(); - $pool = $pools['pools-'.$key.'-'.$database]['pool']; - $dsn = $pools['pools-'.$key.'-'.$database]['dsn']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - $adapter->setDatabase($dsn->getPath()); - + $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ @@ -126,7 +111,7 @@ Http::get('/v1/health/db') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -Http::get('/v1/health/cache') +App::get('/v1/health/cache') ->desc('Get cache') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -139,8 +124,7 @@ Http::get('/v1/health/cache') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('pools') - ->inject('connections') - ->action(function (Response $response, array $pools, Connections $connections) { + ->action(function (Response $response, Group $pools) { $output = []; @@ -150,142 +134,8 @@ Http::get('/v1/health/cache') foreach ($configs as $key => $config) { foreach ($config as $database) { - $checkStart = \microtime(true); try { - $pool = $pools['pools-cache-' . $database]['pool']; - $dsn = $pools['pools-cache-' . $database]['dsn']; - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); - - - if ($adapter->ping()) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } else { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } - } catch (\Throwable $th) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } finally { - $connections->reclaim(); - } - } - } - - $response->dynamic(new Document([ - 'statuses' => $output, - 'total' => count($output), - ]), Response::MODEL_HEALTH_STATUS_LIST); - }); - -Http::get('/v1/health/queue') - ->desc('Get queue') - ->groups(['api', 'health']) - ->label('scope', 'health.read') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'health') - ->label('sdk.method', 'getQueue') - ->label('sdk.description', '/docs/references/health/get-queue.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) - ->inject('response') - ->inject('pools') - ->inject('connections') - ->action(function (Response $response, array $pools, Connections $connections) { - - $output = []; - - $configs = [ - 'Queue' => Config::getParam('pools-queue'), - ]; - - foreach ($configs as $key => $config) { - $checkStart = \microtime(true); - - foreach ($config as $database) { - try { - $pool = $pools['pools-queue-' . $database]['pool']; - $dsn = $pools['pools-queue-' . $database]['dsn']; - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapter = new Connection\Redis($dsn->getHost(), $dsn->getPort()); - if ($adapter->ping()) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'pass', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } else { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } - } catch (\Throwable $th) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); - } finally { - $connections->reclaim(); - } - } - } - - $response->dynamic(new Document([ - 'statuses' => $output, - 'total' => count($output), - ]), Response::MODEL_HEALTH_STATUS_LIST); - }); - -Http::get('/v1/health/pubsub') - ->desc('Get pubsub') - ->groups(['api', 'health']) - ->label('scope', 'health.read') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'health') - ->label('sdk.method', 'getPubSub') - ->label('sdk.description', '/docs/references/health/get-pubsub.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) - ->inject('response') - ->inject('pools') - ->inject('connections') - ->action(function (Response $response, array $pools, Connections $connections) { - - $output = []; - - $configs = [ - 'PubSub' => Config::getParam('pools-pubsub'), - ]; - - foreach ($configs as $key => $config) { - foreach ($config as $database) { - try { - $pool = $pools['pools-pubsub-' . $database]['pool']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapter = new Connection\Redis($connection); + $adapter = $pools->get($database)->pop()->getResource(); $checkStart = \microtime(true); @@ -308,8 +158,6 @@ Http::get('/v1/health/pubsub') 'status' => 'fail', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); - } finally { - $connections->reclaim(); } } } @@ -320,7 +168,121 @@ Http::get('/v1/health/pubsub') ]), Response::MODEL_HEALTH_STATUS_LIST); }); -Http::get('/v1/health/time') +App::get('/v1/health/queue') + ->desc('Get queue') + ->groups(['api', 'health']) + ->label('scope', 'health.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'health') + ->label('sdk.method', 'getQueue') + ->label('sdk.description', '/docs/references/health/get-queue.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) + ->inject('response') + ->inject('pools') + ->action(function (Response $response, Group $pools) { + + $output = []; + + $configs = [ + 'Queue' => Config::getParam('pools-queue'), + ]; + + foreach ($configs as $key => $config) { + foreach ($config as $database) { + try { + $adapter = $pools->get($database)->pop()->getResource(); + + $checkStart = \microtime(true); + + if ($adapter->ping()) { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'pass', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } else { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'fail', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } + } catch (\Throwable $th) { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'fail', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } + } + } + + $response->dynamic(new Document([ + 'statuses' => $output, + 'total' => count($output), + ]), Response::MODEL_HEALTH_STATUS_LIST); + }); + +App::get('/v1/health/pubsub') + ->desc('Get pubsub') + ->groups(['api', 'health']) + ->label('scope', 'health.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'health') + ->label('sdk.method', 'getPubSub') + ->label('sdk.description', '/docs/references/health/get-pubsub.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) + ->inject('response') + ->inject('pools') + ->action(function (Response $response, Group $pools) { + + $output = []; + + $configs = [ + 'PubSub' => Config::getParam('pools-pubsub'), + ]; + + foreach ($configs as $key => $config) { + foreach ($config as $database) { + try { + $adapter = $pools->get($database)->pop()->getResource(); + + $checkStart = \microtime(true); + + if ($adapter->ping()) { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'pass', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } else { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'fail', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } + } catch (\Throwable $th) { + $output[] = new Document([ + 'name' => $key . " ($database)", + 'status' => 'fail', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]); + } + } + } + + $response->dynamic(new Document([ + 'statuses' => $output, + 'total' => count($output), + ]), Response::MODEL_HEALTH_STATUS_LIST); + }); + +App::get('/v1/health/time') ->desc('Get time') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -377,7 +339,7 @@ Http::get('/v1/health/time') $response->dynamic(new Document($output), Response::MODEL_HEALTH_TIME); }); -Http::get('/v1/health/queue/webhooks') +App::get('/v1/health/queue/webhooks') ->desc('Get webhooks queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -404,7 +366,7 @@ Http::get('/v1/health/queue/webhooks') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/logs') +App::get('/v1/health/queue/logs') ->desc('Get logs queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -431,7 +393,7 @@ Http::get('/v1/health/queue/logs') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/certificate') +App::get('/v1/health/certificate') ->desc('Get the SSL certificate for a domain') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -481,7 +443,7 @@ Http::get('/v1/health/certificate') ]), Response::MODEL_HEALTH_CERTIFICATE); }, ['response']); -Http::get('/v1/health/queue/certificates') +App::get('/v1/health/queue/certificates') ->desc('Get certificates queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -508,7 +470,7 @@ Http::get('/v1/health/queue/certificates') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/builds') +App::get('/v1/health/queue/builds') ->desc('Get builds queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -535,7 +497,7 @@ Http::get('/v1/health/queue/builds') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/databases') +App::get('/v1/health/queue/databases') ->desc('Get databases queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -563,7 +525,7 @@ Http::get('/v1/health/queue/databases') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/deletes') +App::get('/v1/health/queue/deletes') ->desc('Get deletes queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -590,7 +552,7 @@ Http::get('/v1/health/queue/deletes') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/mails') +App::get('/v1/health/queue/mails') ->desc('Get mails queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -617,7 +579,7 @@ Http::get('/v1/health/queue/mails') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/messaging') +App::get('/v1/health/queue/messaging') ->desc('Get messaging queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -644,7 +606,7 @@ Http::get('/v1/health/queue/messaging') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/migrations') +App::get('/v1/health/queue/migrations') ->desc('Get migrations queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -671,7 +633,7 @@ Http::get('/v1/health/queue/migrations') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/functions') +App::get('/v1/health/queue/functions') ->desc('Get functions queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -698,7 +660,7 @@ Http::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -Http::get('/v1/health/queue/usage') +App::get('/v1/health/queue/usage') ->desc('Get usage queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -725,7 +687,7 @@ Http::get('/v1/health/queue/usage') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -Http::get('/v1/health/queue/usage-dump') +App::get('/v1/health/queue/usage-dump') ->desc('Get usage dump queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -752,7 +714,7 @@ Http::get('/v1/health/queue/usage-dump') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -Http::get('/v1/health/storage/local') +App::get('/v1/health/storage/local') ->desc('Get local storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -795,7 +757,7 @@ Http::get('/v1/health/storage/local') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -Http::get('/v1/health/storage') +App::get('/v1/health/storage') ->desc('Get storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -836,7 +798,7 @@ Http::get('/v1/health/storage') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); -Http::get('/v1/health/anti-virus') +App::get('/v1/health/anti-virus') ->desc('Get antivirus') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -875,7 +837,7 @@ Http::get('/v1/health/anti-virus') $response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS); }); -Http::get('/v1/health/queue/failed/:name') +App::get('/v1/health/queue/failed/:name') ->desc('Get number of failed queue jobs') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -916,7 +878,7 @@ Http::get('/v1/health/queue/failed/:name') $response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE); }); -Http::get('/v1/health/stats') // Currently only used internally +App::get('/v1/health/stats') // Currently only used internally ->desc('Get system stats') ->groups(['api', 'health']) ->label('scope', 'root') diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index fcaf0c03cb..2917bc8416 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -3,12 +3,12 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Document; -use Utopia\Http\Http; use Utopia\Locale\Locale; -Http::get('/v1/locale') +App::get('/v1/locale') ->desc('Get user locale') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -68,8 +68,8 @@ Http::get('/v1/locale') $response->dynamic(new Document($output), Response::MODEL_LOCALE); }); -Http::get('/v1/locale/codes') - ->desc('List Locale Codes') +App::get('/v1/locale/codes') + ->desc('List locale codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -90,7 +90,7 @@ Http::get('/v1/locale/codes') ]), Response::MODEL_LOCALE_CODE_LIST); }); -Http::get('/v1/locale/countries') +App::get('/v1/locale/countries') ->desc('List countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -123,7 +123,7 @@ Http::get('/v1/locale/countries') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -Http::get('/v1/locale/countries/eu') +App::get('/v1/locale/countries/eu') ->desc('List EU countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -158,7 +158,7 @@ Http::get('/v1/locale/countries/eu') $response->dynamic(new Document(['countries' => $output, 'total' => \count($output)]), Response::MODEL_COUNTRY_LIST); }); -Http::get('/v1/locale/countries/phones') +App::get('/v1/locale/countries/phones') ->desc('List countries phone codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -192,7 +192,7 @@ Http::get('/v1/locale/countries/phones') $response->dynamic(new Document(['phones' => $output, 'total' => \count($output)]), Response::MODEL_PHONE_LIST); }); -Http::get('/v1/locale/continents') +App::get('/v1/locale/continents') ->desc('List continents') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -224,7 +224,7 @@ Http::get('/v1/locale/continents') $response->dynamic(new Document(['continents' => $output, 'total' => \count($output)]), Response::MODEL_CONTINENT_LIST); }); -Http::get('/v1/locale/currencies') +App::get('/v1/locale/currencies') ->desc('List currencies') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -247,7 +247,7 @@ Http::get('/v1/locale/currencies') }); -Http::get('/v1/locale/languages') +App::get('/v1/locale/languages') ->desc('List languages') ->groups(['api', 'locale']) ->label('scope', 'locale.read') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 8beb38c7ca..7da0348a8f 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -20,6 +20,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Targets; use Appwrite\Utopia\Database\Validator\Queries\Topics; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Audit\Audit; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -29,27 +30,25 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\JSON; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Boolean; +use Utopia\Validator\Integer; +use Utopia\Validator\JSON; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use function Swoole\Coroutine\batch; -Http::post('/v1/messaging/providers/mailgun') +App::post('/v1/messaging/providers/mailgun') ->desc('Create Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -136,7 +135,7 @@ Http::post('/v1/messaging/providers/mailgun') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/sendgrid') +App::post('/v1/messaging/providers/sendgrid') ->desc('Create Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -211,7 +210,7 @@ Http::post('/v1/messaging/providers/sendgrid') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/smtp') +App::post('/v1/messaging/providers/smtp') ->desc('Create SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -299,7 +298,7 @@ Http::post('/v1/messaging/providers/smtp') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/msg91') +App::post('/v1/messaging/providers/msg91') ->desc('Create Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -375,7 +374,7 @@ Http::post('/v1/messaging/providers/msg91') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/telesign') +App::post('/v1/messaging/providers/telesign') ->desc('Create Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -452,7 +451,7 @@ Http::post('/v1/messaging/providers/telesign') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/textmagic') +App::post('/v1/messaging/providers/textmagic') ->desc('Create Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -529,7 +528,7 @@ Http::post('/v1/messaging/providers/textmagic') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/twilio') +App::post('/v1/messaging/providers/twilio') ->desc('Create Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -606,7 +605,7 @@ Http::post('/v1/messaging/providers/twilio') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/vonage') +App::post('/v1/messaging/providers/vonage') ->desc('Create Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -683,7 +682,7 @@ Http::post('/v1/messaging/providers/vonage') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/fcm') +App::post('/v1/messaging/providers/fcm') ->desc('Create FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -746,7 +745,7 @@ Http::post('/v1/messaging/providers/fcm') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::post('/v1/messaging/providers/apns') +App::post('/v1/messaging/providers/apns') ->desc('Create APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') @@ -832,7 +831,7 @@ Http::post('/v1/messaging/providers/apns') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::get('/v1/messaging/providers') +App::get('/v1/messaging/providers') ->desc('List providers') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -847,8 +846,7 @@ Http::get('/v1/messaging/providers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -869,7 +867,7 @@ Http::get('/v1/messaging/providers') if ($cursor) { $providerId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); @@ -884,7 +882,7 @@ Http::get('/v1/messaging/providers') ]), Response::MODEL_PROVIDER_LIST); }); -Http::get('/v1/messaging/providers/:providerId/logs') +App::get('/v1/messaging/providers/:providerId/logs') ->desc('List provider logs') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -972,7 +970,7 @@ Http::get('/v1/messaging/providers/:providerId/logs') ]), Response::MODEL_LOG_LIST); }); -Http::get('/v1/messaging/providers/:providerId') +App::get('/v1/messaging/providers/:providerId') ->desc('Get provider') ->groups(['api', 'messaging']) ->label('scope', 'providers.read') @@ -996,7 +994,7 @@ Http::get('/v1/messaging/providers/:providerId') $response->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/mailgun/:providerId') +App::patch('/v1/messaging/providers/mailgun/:providerId') ->desc('Update Mailgun provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1102,7 +1100,7 @@ Http::patch('/v1/messaging/providers/mailgun/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/sendgrid/:providerId') +App::patch('/v1/messaging/providers/sendgrid/:providerId') ->desc('Update Sendgrid provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1193,7 +1191,7 @@ Http::patch('/v1/messaging/providers/sendgrid/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/smtp/:providerId') +App::patch('/v1/messaging/providers/smtp/:providerId') ->desc('Update SMTP provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1315,7 +1313,7 @@ Http::patch('/v1/messaging/providers/smtp/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/msg91/:providerId') +App::patch('/v1/messaging/providers/msg91/:providerId') ->desc('Update Msg91 provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1395,7 +1393,7 @@ Http::patch('/v1/messaging/providers/msg91/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/telesign/:providerId') +App::patch('/v1/messaging/providers/telesign/:providerId') ->desc('Update Telesign provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1477,7 +1475,7 @@ Http::patch('/v1/messaging/providers/telesign/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/textmagic/:providerId') +App::patch('/v1/messaging/providers/textmagic/:providerId') ->desc('Update Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1559,7 +1557,7 @@ Http::patch('/v1/messaging/providers/textmagic/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/twilio/:providerId') +App::patch('/v1/messaging/providers/twilio/:providerId') ->desc('Update Twilio provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1641,7 +1639,7 @@ Http::patch('/v1/messaging/providers/twilio/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/vonage/:providerId') +App::patch('/v1/messaging/providers/vonage/:providerId') ->desc('Update Vonage provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1723,7 +1721,7 @@ Http::patch('/v1/messaging/providers/vonage/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::patch('/v1/messaging/providers/fcm/:providerId') +App::patch('/v1/messaging/providers/fcm/:providerId') ->desc('Update FCM provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1792,7 +1790,7 @@ Http::patch('/v1/messaging/providers/fcm/:providerId') }); -Http::patch('/v1/messaging/providers/apns/:providerId') +App::patch('/v1/messaging/providers/apns/:providerId') ->desc('Update APNS provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') @@ -1887,7 +1885,7 @@ Http::patch('/v1/messaging/providers/apns/:providerId') ->dynamic($provider, Response::MODEL_PROVIDER); }); -Http::delete('/v1/messaging/providers/:providerId') +App::delete('/v1/messaging/providers/:providerId') ->desc('Delete provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.delete') @@ -1922,7 +1920,7 @@ Http::delete('/v1/messaging/providers/:providerId') ->noContent(); }); -Http::post('/v1/messaging/topics') +App::post('/v1/messaging/topics') ->desc('Create topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.create') @@ -1965,7 +1963,7 @@ Http::post('/v1/messaging/topics') ->dynamic($topic, Response::MODEL_TOPIC); }); -Http::get('/v1/messaging/topics') +App::get('/v1/messaging/topics') ->desc('List topics') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -1980,8 +1978,7 @@ Http::get('/v1/messaging/topics') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2002,7 +1999,7 @@ Http::get('/v1/messaging/topics') if ($cursor) { $topicId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found."); @@ -2017,7 +2014,7 @@ Http::get('/v1/messaging/topics') ]), Response::MODEL_TOPIC_LIST); }); -Http::get('/v1/messaging/topics/:topicId/logs') +App::get('/v1/messaging/topics/:topicId/logs') ->desc('List topic logs') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2034,8 +2031,7 @@ Http::get('/v1/messaging/topics/:topicId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $topic = $dbForProject->getDocument('topics', $topicId); if ($topic->isEmpty()) { @@ -2107,7 +2103,7 @@ Http::get('/v1/messaging/topics/:topicId/logs') ]), Response::MODEL_LOG_LIST); }); -Http::get('/v1/messaging/topics/:topicId') +App::get('/v1/messaging/topics/:topicId') ->desc('Get topic') ->groups(['api', 'messaging']) ->label('scope', 'topics.read') @@ -2132,7 +2128,7 @@ Http::get('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -Http::patch('/v1/messaging/topics/:topicId') +App::patch('/v1/messaging/topics/:topicId') ->desc('Update topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.update') @@ -2176,7 +2172,7 @@ Http::patch('/v1/messaging/topics/:topicId') ->dynamic($topic, Response::MODEL_TOPIC); }); -Http::delete('/v1/messaging/topics/:topicId') +App::delete('/v1/messaging/topics/:topicId') ->desc('Delete topic') ->groups(['api', 'messaging']) ->label('audits.event', 'topic.delete') @@ -2216,7 +2212,7 @@ Http::delete('/v1/messaging/topics/:topicId') ->noContent(); }); -Http::post('/v1/messaging/topics/:topicId/subscribers') +App::post('/v1/messaging/topics/:topicId/subscribers') ->desc('Create subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.create') @@ -2236,27 +2232,28 @@ Http::post('/v1/messaging/topics/:topicId/subscribers') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { + ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) { $subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId; - $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); } - if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) { - throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription()); + $validator = new Authorization('subscribe'); + + if (!$validator->isValid($topic->getAttribute('subscribe'))) { + throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); } - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber = new Document([ '$id' => $subscriberId, @@ -2289,7 +2286,7 @@ Http::post('/v1/messaging/topics/:topicId/subscribers') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute( + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2311,7 +2308,7 @@ Http::post('/v1/messaging/topics/:topicId/subscribers') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -Http::get('/v1/messaging/topics/:topicId/subscribers') +App::get('/v1/messaging/topics/:topicId/subscribers') ->desc('List subscribers') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2327,8 +2324,7 @@ Http::get('/v1/messaging/topics/:topicId/subscribers') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { + ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -2339,7 +2335,7 @@ Http::get('/v1/messaging/topics/:topicId/subscribers') $queries[] = Query::search('search', $search); } - $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2357,7 +2353,7 @@ Http::get('/v1/messaging/topics/:topicId/subscribers') if ($cursor) { $subscriberId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found."); @@ -2368,10 +2364,10 @@ Http::get('/v1/messaging/topics/:topicId/subscribers') $subscribers = $dbForProject->find('subscribers', $queries); - $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) { - return function () use ($subscriber, $dbForProject, $authorization) { - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { + return function () use ($subscriber, $dbForProject) { + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); return $subscriber ->setAttribute('target', $target) @@ -2386,7 +2382,7 @@ Http::get('/v1/messaging/topics/:topicId/subscribers') ]), Response::MODEL_SUBSCRIBER_LIST); }); -Http::get('/v1/messaging/subscribers/:subscriberId/logs') +App::get('/v1/messaging/subscribers/:subscriberId/logs') ->desc('List subscriber logs') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2403,8 +2399,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); if ($subscriber->isEmpty()) { @@ -2476,7 +2471,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs') ]), Response::MODEL_LOG_LIST); }); -Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Get subscriber') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') @@ -2491,9 +2486,8 @@ Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response, Authorization $authorization) { - $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2505,8 +2499,8 @@ Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } - $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); - $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); $subscriber ->setAttribute('target', $target) @@ -2516,7 +2510,7 @@ Http::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') +App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->desc('Delete subscriber') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.delete') @@ -2535,9 +2529,8 @@ Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response, Authorization $authorization) { - $topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId)); + ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); @@ -2560,7 +2553,7 @@ Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), }; - $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute( + Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute( 'topics', $topicId, $totalAttribute, @@ -2576,7 +2569,7 @@ Http::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') ->noContent(); }); -Http::post('/v1/messaging/messages/email') +App::post('/v1/messaging/messages/email') ->desc('Create email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2728,7 +2721,7 @@ Http::post('/v1/messaging/messages/email') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::post('/v1/messaging/messages/sms') +App::post('/v1/messaging/messages/sms') ->desc('Create SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -2844,7 +2837,7 @@ Http::post('/v1/messaging/messages/sms') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::post('/v1/messaging/messages/push') +App::post('/v1/messaging/messages/push') ->desc('Create push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.create') @@ -3020,7 +3013,7 @@ Http::post('/v1/messaging/messages/push') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::get('/v1/messaging/messages') +App::get('/v1/messaging/messages') ->desc('List messages') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3035,8 +3028,7 @@ Http::get('/v1/messaging/messages') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->inject('authorization') - ->action(function (array $queries, string $search, Database $dbForProject, Response $response, Authorization $authorization) { + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -3057,7 +3049,7 @@ Http::get('/v1/messaging/messages') if ($cursor) { $messageId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); @@ -3072,7 +3064,7 @@ Http::get('/v1/messaging/messages') ]), Response::MODEL_MESSAGE_LIST); }); -Http::get('/v1/messaging/messages/:messageId/logs') +App::get('/v1/messaging/messages/:messageId/logs') ->desc('List message logs') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3089,8 +3081,7 @@ Http::get('/v1/messaging/messages/:messageId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -3162,7 +3153,7 @@ Http::get('/v1/messaging/messages/:messageId/logs') ]), Response::MODEL_LOG_LIST); }); -Http::get('/v1/messaging/messages/:messageId/targets') +App::get('/v1/messaging/messages/:messageId/targets') ->desc('List message targets') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3227,7 +3218,7 @@ Http::get('/v1/messaging/messages/:messageId/targets') ]), Response::MODEL_TARGET_LIST); }); -Http::get('/v1/messaging/messages/:messageId') +App::get('/v1/messaging/messages/:messageId') ->desc('Get message') ->groups(['api', 'messaging']) ->label('scope', 'messages.read') @@ -3251,7 +3242,7 @@ Http::get('/v1/messaging/messages/:messageId') $response->dynamic($message, Response::MODEL_MESSAGE); }); -Http::patch('/v1/messaging/messages/email/:messageId') +App::patch('/v1/messaging/messages/email/:messageId') ->desc('Update email') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3451,7 +3442,7 @@ Http::patch('/v1/messaging/messages/email/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::patch('/v1/messaging/messages/sms/:messageId') +App::patch('/v1/messaging/messages/sms/:messageId') ->desc('Update SMS') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3606,7 +3597,7 @@ Http::patch('/v1/messaging/messages/sms/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::patch('/v1/messaging/messages/push/:messageId') +App::patch('/v1/messaging/messages/push/:messageId') ->desc('Update push notification') ->groups(['api', 'messaging']) ->label('audits.event', 'message.update') @@ -3844,7 +3835,7 @@ Http::patch('/v1/messaging/messages/push/:messageId') ->dynamic($message, Response::MODEL_MESSAGE); }); -Http::delete('/v1/messaging/messages/:messageId') +App::delete('/v1/messaging/messages/:messageId') ->desc('Delete message') ->groups(['api', 'messaging']) ->label('audits.event', 'message.delete') diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 5e08e197ae..bb89d4a26f 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -9,6 +9,7 @@ use Appwrite\Role; use Appwrite\Utopia\Database\Validator\Queries\Migrations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -16,22 +17,21 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Host; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\URL; -use Utopia\Http\Validator\WhiteList; use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\Firebase; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Host; +use Utopia\Validator\Integer; +use Utopia\Validator\Text; +use Utopia\Validator\URL; +use Utopia\Validator\WhiteList; include_once __DIR__ . '/../shared/api.php'; -Http::post('/v1/migrations/appwrite') +App::post('/v1/migrations/appwrite') ->groups(['api', 'migrations']) ->desc('Migrate Appwrite data') ->label('scope', 'migrations.write') @@ -86,7 +86,7 @@ Http::post('/v1/migrations/appwrite') ->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::post('/v1/migrations/firebase/oauth') +App::post('/v1/migrations/firebase/oauth') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (OAuth)') ->label('scope', 'migrations.write') @@ -120,7 +120,7 @@ Http::post('/v1/migrations/firebase/oauth') Query::equal('provider', ['firebase']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -189,7 +189,7 @@ Http::post('/v1/migrations/firebase/oauth') ->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::post('/v1/migrations/firebase') +App::post('/v1/migrations/firebase') ->groups(['api', 'migrations']) ->desc('Migrate Firebase data (Service Account)') ->label('scope', 'migrations.write') @@ -250,7 +250,7 @@ Http::post('/v1/migrations/firebase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::post('/v1/migrations/supabase') +App::post('/v1/migrations/supabase') ->groups(['api', 'migrations']) ->desc('Migrate Supabase data') ->label('scope', 'migrations.write') @@ -311,7 +311,7 @@ Http::post('/v1/migrations/supabase') ->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::post('/v1/migrations/nhost') +App::post('/v1/migrations/nhost') ->groups(['api', 'migrations']) ->desc('Migrate NHost data') ->label('scope', 'migrations.write') @@ -374,7 +374,7 @@ Http::post('/v1/migrations/nhost') ->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::get('/v1/migrations') +App::get('/v1/migrations') ->groups(['api', 'migrations']) ->desc('List migrations') ->label('scope', 'migrations.read') @@ -427,7 +427,7 @@ Http::get('/v1/migrations') ]), Response::MODEL_MIGRATION_LIST); }); -Http::get('/v1/migrations/:migrationId') +App::get('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Get migration') ->label('scope', 'migrations.read') @@ -451,7 +451,7 @@ Http::get('/v1/migrations/:migrationId') $response->dynamic($migration, Response::MODEL_MIGRATION); }); -Http::get('/v1/migrations/appwrite/report') +App::get('/v1/migrations/appwrite/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Appwrite data') ->label('scope', 'migrations.write') @@ -493,7 +493,7 @@ Http::get('/v1/migrations/appwrite/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -Http::get('/v1/migrations/firebase/report') +App::get('/v1/migrations/firebase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data') ->label('scope', 'migrations.write') @@ -540,7 +540,7 @@ Http::get('/v1/migrations/firebase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -Http::get('/v1/migrations/firebase/report/oauth') +App::get('/v1/migrations/firebase/report/oauth') ->groups(['api', 'migrations']) ->desc('Generate a report on Firebase data using OAuth') ->label('scope', 'migrations.write') @@ -569,7 +569,7 @@ Http::get('/v1/migrations/firebase/report/oauth') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -631,7 +631,7 @@ Http::get('/v1/migrations/firebase/report/oauth') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -Http::get('/v1/migrations/firebase/connect') +App::get('/v1/migrations/firebase/connect') ->desc('Authorize with Firebase') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') @@ -673,7 +673,7 @@ Http::get('/v1/migrations/firebase/connect') ->redirect($url); }); -Http::get('/v1/migrations/firebase/redirect') +App::get('/v1/migrations/firebase/redirect') ->desc('Capture and receive data on Firebase authorization') ->groups(['api', 'migrations']) ->label('scope', 'public') @@ -744,7 +744,7 @@ Http::get('/v1/migrations/firebase/redirect') Query::equal('providerEmail', [$email]), ]); - if (!$identity->isEmpty()) { + if ($identity !== false && !$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -785,8 +785,8 @@ Http::get('/v1/migrations/firebase/redirect') ->redirect($redirect); }); -Http::get('/v1/migrations/firebase/projects') - ->desc('List Firebase Projects') +App::get('/v1/migrations/firebase/projects') + ->desc('List Firebase projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -813,7 +813,7 @@ Http::get('/v1/migrations/firebase/projects') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -874,8 +874,8 @@ Http::get('/v1/migrations/firebase/projects') ]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST); }); -Http::get('/v1/migrations/firebase/deauthorize') - ->desc('Revoke Appwrite\'s authorization to access Firebase Projects') +App::get('/v1/migrations/firebase/deauthorize') + ->desc('Revoke Appwrite\'s authorization to access Firebase projects') ->groups(['api', 'migrations']) ->label('scope', 'migrations.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -893,7 +893,7 @@ Http::get('/v1/migrations/firebase/deauthorize') Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND } @@ -902,7 +902,7 @@ Http::get('/v1/migrations/firebase/deauthorize') $response->noContent(); }); -Http::get('/v1/migrations/supabase/report') +App::get('/v1/migrations/supabase/report') ->groups(['api', 'migrations']) ->desc('Generate a report on Supabase Data') ->label('scope', 'migrations.write') @@ -945,7 +945,7 @@ Http::get('/v1/migrations/supabase/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -Http::get('/v1/migrations/nhost/report') +App::get('/v1/migrations/nhost/report') ->groups(['api', 'migrations']) ->desc('Generate a report on NHost Data') ->label('scope', 'migrations.write') @@ -988,7 +988,7 @@ Http::get('/v1/migrations/nhost/report') ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); -Http::patch('/v1/migrations/:migrationId') +App::patch('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Retry migration') ->label('scope', 'migrations.write') @@ -1033,7 +1033,7 @@ Http::patch('/v1/migrations/:migrationId') $response->noContent(); }); -Http::delete('/v1/migrations/:migrationId') +App::delete('/v1/migrations/:migrationId') ->groups(['api', 'migrations']) ->desc('Delete migration') ->label('scope', 'migrations.write') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 8a6f4d942e..6053326308 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -2,6 +2,7 @@ use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; @@ -12,11 +13,10 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DateTimeValidator; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; -Http::get('/v1/project/usage') +App::get('/v1/project/usage') ->desc('Get project usage stats') ->groups(['api', 'usage']) ->label('scope', 'projects.read') @@ -31,8 +31,7 @@ Http::get('/v1/project/usage') ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) { $stats = $total = $usage = []; $format = 'Y-m-d 00:00:00'; $firstDay = (new DateTime($startDate))->format($format); @@ -79,7 +78,7 @@ Http::get('/v1/project/usage') '1d' => 'Y-m-d\T00:00:00.000P', }; - $authorization->skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { + Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) { foreach ($metrics['total'] as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -297,6 +296,8 @@ Http::get('/v1/project/usage') 'buildsStorageTotal' => $total[METRIC_BUILDS_STORAGE], 'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE], 'executionsBreakdown' => $executionsBreakdown, + 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, + 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, @@ -307,8 +308,8 @@ Http::get('/v1/project/usage') // Variables -Http::post('/v1/project/variables') - ->desc('Create Variable') +App::post('/v1/project/variables') + ->desc('Create variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('audits.event', 'variable.create') @@ -362,8 +363,8 @@ Http::post('/v1/project/variables') ->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::get('/v1/project/variables') - ->desc('List Variables') +App::get('/v1/project/variables') + ->desc('List variables') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -387,8 +388,8 @@ Http::get('/v1/project/variables') ]), Response::MODEL_VARIABLE_LIST); }); -Http::get('/v1/project/variables/:variableId') - ->desc('Get Variable') +App::get('/v1/project/variables/:variableId') + ->desc('Get variable') ->groups(['api']) ->label('scope', 'projects.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -411,8 +412,8 @@ Http::get('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::put('/v1/project/variables/:variableId') - ->desc('Update Variable') +App::put('/v1/project/variables/:variableId') + ->desc('Update variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -457,8 +458,8 @@ Http::put('/v1/project/variables/:variableId') $response->dynamic($variable, Response::MODEL_VARIABLE); }); -Http::delete('/v1/project/variables/:variableId') - ->desc('Delete Variable') +App::delete('/v1/project/variables/:variableId') + ->desc('Delete variable') ->groups(['api']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 8897752f2e..934793410b 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -13,16 +13,14 @@ use Appwrite\Network\Validator\Origin; use Appwrite\Template\Template; use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; -use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; use Utopia\Abuse\Adapters\Database\TimeLimit; +use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; use Utopia\Config\Config; -use Utopia\Database\Adapter\MariaDB; -use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -32,25 +30,24 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Domains\Validator\PublicDomain; use Utopia\DSN\DSN; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Hostname; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Multiple; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\URL; -use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; +use Utopia\Pools\Group; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Boolean; +use Utopia\Validator\Hostname; +use Utopia\Validator\Integer; +use Utopia\Validator\Multiple; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\URL; +use Utopia\Validator\WhiteList; -Http::init() +App::init() ->groups(['projects']) ->inject('project') ->action(function (Document $project) { @@ -59,7 +56,7 @@ Http::init() } }); -Http::post('/v1/projects') +App::post('/v1/projects') ->desc('Create project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.create') @@ -89,9 +86,8 @@ Http::post('/v1/projects') ->inject('cache') ->inject('pools') ->inject('hooks') - ->inject('authorization') - ->inject('connections') - ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, array $pools, Hooks $hooks, Authorization $authorization, Connections $connections) { + ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) { + $team = $dbForConsole->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -193,21 +189,8 @@ Http::post('/v1/projects') $dsn = new DSN('mysql://' . $dsn); } - $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; - $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapter = match ($connectionDsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - - $adapter->setDatabase($connectionDsn->getPath()); - + $adapter = $pools->get($dsn->getHost())->pop()->getResource(); $dbForProject = new Database($adapter, $cache); - $dbForProject->setAuthorization($authorization); if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { $dbForProject @@ -225,8 +208,10 @@ Http::post('/v1/projects') $audit = new Audit($dbForProject); $audit->setup(); + $abuse = new TimeLimit('', 0, 1, $dbForProject); $abuse->setup(); + /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; @@ -249,17 +234,17 @@ Http::post('/v1/projects') // Collection already exists } } - $connections->reclaim(); + // Hook allowing instant project mirroring during migration // Outside of migration, hook is not registered and has no effect - $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); + $hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($project, Response::MODEL_PROJECT); }); -Http::get('/v1/projects') +App::get('/v1/projects') ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -274,6 +259,7 @@ Http::get('/v1/projects') ->inject('response') ->inject('dbForConsole') ->action(function (array $queries, string $search, Response $response, Database $dbForConsole) { + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -311,7 +297,7 @@ Http::get('/v1/projects') ]), Response::MODEL_PROJECT_LIST); }); -Http::get('/v1/projects/:projectId') +App::get('/v1/projects/:projectId') ->desc('Get project') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -325,6 +311,7 @@ Http::get('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -334,7 +321,7 @@ Http::get('/v1/projects/:projectId') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId') +App::patch('/v1/projects/:projectId') ->desc('Update project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -358,34 +345,31 @@ Http::patch('/v1/projects/:projectId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project = $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('name', $name) - ->setAttribute('description', $description) - ->setAttribute('logo', $logo) - ->setAttribute('url', $url) - ->setAttribute('legalName', $legalName) - ->setAttribute('legalCountry', $legalCountry) - ->setAttribute('legalState', $legalState) - ->setAttribute('legalCity', $legalCity) - ->setAttribute('legalAddress', $legalAddress) - ->setAttribute('legalTaxId', $legalTaxId) - ->setAttribute('search', implode(' ', [$projectId, $name])) - ); + $project = $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('name', $name) + ->setAttribute('description', $description) + ->setAttribute('logo', $logo) + ->setAttribute('url', $url) + ->setAttribute('legalName', $legalName) + ->setAttribute('legalCountry', $legalCountry) + ->setAttribute('legalState', $legalState) + ->setAttribute('legalCity', $legalCity) + ->setAttribute('legalAddress', $legalAddress) + ->setAttribute('legalTaxId', $legalTaxId) + ->setAttribute('search', implode(' ', [$projectId, $name]))); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/team') - ->desc('Update Project Team') +App::patch('/v1/projects/:projectId/team') + ->desc('Update project team') ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) @@ -399,6 +383,7 @@ Http::patch('/v1/projects/:projectId/team') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); $team = $dbForConsole->getDocument('teams', $teamId); @@ -451,7 +436,7 @@ Http::patch('/v1/projects/:projectId/team') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/service') +App::patch('/v1/projects/:projectId/service') ->desc('Update service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -467,6 +452,7 @@ Http::patch('/v1/projects/:projectId/service') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -481,7 +467,7 @@ Http::patch('/v1/projects/:projectId/service') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/service/all') +App::patch('/v1/projects/:projectId/service/all') ->desc('Update all service status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -496,6 +482,7 @@ Http::patch('/v1/projects/:projectId/service/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -514,7 +501,7 @@ Http::patch('/v1/projects/:projectId/service/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/api') +App::patch('/v1/projects/:projectId/api') ->desc('Update API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -530,6 +517,7 @@ Http::patch('/v1/projects/:projectId/api') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -544,7 +532,7 @@ Http::patch('/v1/projects/:projectId/api') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/api/all') +App::patch('/v1/projects/:projectId/api/all') ->desc('Update all API status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -559,6 +547,7 @@ Http::patch('/v1/projects/:projectId/api/all') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -577,7 +566,7 @@ Http::patch('/v1/projects/:projectId/api/all') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/oauth2') +App::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -595,6 +584,7 @@ Http::patch('/v1/projects/:projectId/oauth2') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -620,7 +610,7 @@ Http::patch('/v1/projects/:projectId/oauth2') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/session-alerts') +App::patch('/v1/projects/:projectId/auth/session-alerts') ->desc('Update project sessions emails') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -651,7 +641,7 @@ Http::patch('/v1/projects/:projectId/auth/session-alerts') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/limit') +App::patch('/v1/projects/:projectId/auth/limit') ->desc('Update project users limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -666,6 +656,7 @@ Http::patch('/v1/projects/:projectId/auth/limit') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -675,17 +666,13 @@ Http::patch('/v1/projects/:projectId/auth/limit') $auths = $project->getAttribute('auths', []); $auths['limit'] = $limit; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/duration') +App::patch('/v1/projects/:projectId/auth/duration') ->desc('Update project authentication duration') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -700,6 +687,7 @@ Http::patch('/v1/projects/:projectId/auth/duration') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -709,17 +697,13 @@ Http::patch('/v1/projects/:projectId/auth/duration') $auths = $project->getAttribute('auths', []); $auths['duration'] = $duration; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/:method') +App::patch('/v1/projects/:projectId/auth/:method') ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -735,9 +719,10 @@ Http::patch('/v1/projects/:projectId/auth/:method') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); - $authConfig = Config::getParam('auth')[$method] ?? []; - $authKey = $authConfig['key'] ?? ''; + $auth = Config::getParam('auth')[$method] ?? []; + $authKey = $auth['key'] ?? ''; $status = ($status === '1' || $status === 'true' || $status === 1 || $status === true); if ($project->isEmpty()) { @@ -752,7 +737,7 @@ Http::patch('/v1/projects/:projectId/auth/:method') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/password-history') +App::patch('/v1/projects/:projectId/auth/password-history') ->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -767,6 +752,7 @@ Http::patch('/v1/projects/:projectId/auth/password-history') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -776,17 +762,13 @@ Http::patch('/v1/projects/:projectId/auth/password-history') $auths = $project->getAttribute('auths', []); $auths['passwordHistory'] = $limit; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/password-dictionary') +App::patch('/v1/projects/:projectId/auth/password-dictionary') ->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -801,6 +783,7 @@ Http::patch('/v1/projects/:projectId/auth/password-dictionary') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -810,17 +793,13 @@ Http::patch('/v1/projects/:projectId/auth/password-dictionary') $auths = $project->getAttribute('auths', []); $auths['passwordDictionary'] = $enabled; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/personal-data') +App::patch('/v1/projects/:projectId/auth/personal-data') ->desc('Enable or disable checking user passwords for similarity with their personal data.') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -835,6 +814,7 @@ Http::patch('/v1/projects/:projectId/auth/personal-data') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -844,17 +824,13 @@ Http::patch('/v1/projects/:projectId/auth/personal-data') $auths = $project->getAttribute('auths', []); $auths['personalDataCheck'] = $enabled; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/max-sessions') +App::patch('/v1/projects/:projectId/auth/max-sessions') ->desc('Update project user sessions limit') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -869,6 +845,7 @@ Http::patch('/v1/projects/:projectId/auth/max-sessions') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -878,17 +855,13 @@ Http::patch('/v1/projects/:projectId/auth/max-sessions') $auths = $project->getAttribute('auths', []); $auths['maxSessions'] = $limit; - $dbForConsole->updateDocument( - 'projects', - $project->getId(), - $project - ->setAttribute('auths', $auths) - ); + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::patch('/v1/projects/:projectId/auth/mock-numbers') +App::patch('/v1/projects/:projectId/auth/mock-numbers') ->desc('Update the mock numbers for the project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -927,7 +900,7 @@ Http::patch('/v1/projects/:projectId/auth/mock-numbers') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::delete('/v1/projects/:projectId') +App::delete('/v1/projects/:projectId') ->desc('Delete project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.delete') @@ -963,7 +936,7 @@ Http::delete('/v1/projects/:projectId') // Webhooks -Http::post('/v1/projects/:projectId/webhooks') +App::post('/v1/projects/:projectId/webhooks') ->desc('Create webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -984,13 +957,14 @@ Http::post('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $security = (bool)filter_var($security, FILTER_VALIDATE_BOOLEAN); + $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); $webhook = new Document([ '$id' => ID::unique(), @@ -1020,7 +994,7 @@ Http::post('/v1/projects/:projectId/webhooks') ->dynamic($webhook, Response::MODEL_WEBHOOK); }); -Http::get('/v1/projects/:projectId/webhooks') +App::get('/v1/projects/:projectId/webhooks') ->desc('List webhooks') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1034,6 +1008,7 @@ Http::get('/v1/projects/:projectId/webhooks') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1051,7 +1026,7 @@ Http::get('/v1/projects/:projectId/webhooks') ]), Response::MODEL_WEBHOOK_LIST); }); -Http::get('/v1/projects/:projectId/webhooks/:webhookId') +App::get('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1066,6 +1041,7 @@ Http::get('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1077,14 +1053,14 @@ Http::get('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook->isEmpty()) { + if ($webhook === false || $webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -Http::put('/v1/projects/:projectId/webhooks/:webhookId') +App::put('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1106,6 +1082,7 @@ Http::put('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1119,7 +1096,7 @@ Http::put('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook->isEmpty()) { + if ($webhook === false || $webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1142,7 +1119,7 @@ Http::put('/v1/projects/:projectId/webhooks/:webhookId') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') +App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->desc('Update webhook signature key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1157,6 +1134,7 @@ Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1168,7 +1146,7 @@ Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook->isEmpty()) { + if ($webhook === false || $webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1180,7 +1158,7 @@ Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); -Http::delete('/v1/projects/:projectId/webhooks/:webhookId') +App::delete('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1194,6 +1172,7 @@ Http::delete('/v1/projects/:projectId/webhooks/:webhookId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1205,7 +1184,7 @@ Http::delete('/v1/projects/:projectId/webhooks/:webhookId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($webhook->isEmpty()) { + if ($webhook === false || $webhook->isEmpty()) { throw new Exception(Exception::WEBHOOK_NOT_FOUND); } @@ -1218,7 +1197,7 @@ Http::delete('/v1/projects/:projectId/webhooks/:webhookId') // Keys -Http::post('/v1/projects/:projectId/keys') +App::post('/v1/projects/:projectId/keys') ->desc('Create key') ->groups(['api', 'projects']) ->label('scope', 'keys.write') @@ -1235,6 +1214,7 @@ Http::post('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1267,7 +1247,7 @@ Http::post('/v1/projects/:projectId/keys') ->dynamic($key, Response::MODEL_KEY); }); -Http::get('/v1/projects/:projectId/keys') +App::get('/v1/projects/:projectId/keys') ->desc('List keys') ->groups(['api', 'projects']) ->label('scope', 'keys.read') @@ -1281,6 +1261,7 @@ Http::get('/v1/projects/:projectId/keys') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1298,7 +1279,7 @@ Http::get('/v1/projects/:projectId/keys') ]), Response::MODEL_KEY_LIST); }); -Http::get('/v1/projects/:projectId/keys/:keyId') +App::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get key') ->groups(['api', 'projects']) ->label('scope', 'keys.read') @@ -1313,6 +1294,7 @@ Http::get('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1324,14 +1306,14 @@ Http::get('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key->isEmpty()) { + if ($key === false || $key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } $response->dynamic($key, Response::MODEL_KEY); }); -Http::put('/v1/projects/:projectId/keys/:keyId') +App::put('/v1/projects/:projectId/keys/:keyId') ->desc('Update key') ->groups(['api', 'projects']) ->label('scope', 'keys.write') @@ -1349,6 +1331,7 @@ Http::put('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1360,7 +1343,7 @@ Http::put('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key->isEmpty()) { + if ($key === false || $key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1376,7 +1359,7 @@ Http::put('/v1/projects/:projectId/keys/:keyId') $response->dynamic($key, Response::MODEL_KEY); }); -Http::delete('/v1/projects/:projectId/keys/:keyId') +App::delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete key') ->groups(['api', 'projects']) ->label('scope', 'keys.write') @@ -1390,6 +1373,7 @@ Http::delete('/v1/projects/:projectId/keys/:keyId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1401,7 +1385,7 @@ Http::delete('/v1/projects/:projectId/keys/:keyId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($key->isEmpty()) { + if ($key === false || $key->isEmpty()) { throw new Exception(Exception::KEY_NOT_FOUND); } @@ -1414,7 +1398,7 @@ Http::delete('/v1/projects/:projectId/keys/:keyId') // JWT Keys -Http::post('/v1/projects/:projectId/jwts') +App::post('/v1/projects/:projectId/jwts') ->groups(['api', 'projects']) ->desc('Create JWT') ->label('scope', 'projects.write') @@ -1449,7 +1433,7 @@ Http::post('/v1/projects/:projectId/jwts') // Platforms -Http::post('/v1/projects/:projectId/platforms') +App::post('/v1/projects/:projectId/platforms') ->desc('Create platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.create') @@ -1500,7 +1484,7 @@ Http::post('/v1/projects/:projectId/platforms') ->dynamic($platform, Response::MODEL_PLATFORM); }); -Http::get('/v1/projects/:projectId/platforms') +App::get('/v1/projects/:projectId/platforms') ->desc('List platforms') ->groups(['api', 'projects']) ->label('scope', 'platforms.read') @@ -1514,6 +1498,7 @@ Http::get('/v1/projects/:projectId/platforms') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1531,7 +1516,7 @@ Http::get('/v1/projects/:projectId/platforms') ]), Response::MODEL_PLATFORM_LIST); }); -Http::get('/v1/projects/:projectId/platforms/:platformId') +App::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get platform') ->groups(['api', 'projects']) ->label('scope', 'platforms.read') @@ -1546,6 +1531,7 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1557,14 +1543,14 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform->isEmpty()) { + if ($platform === false || $platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } $response->dynamic($platform, Response::MODEL_PLATFORM); }); -Http::put('/v1/projects/:projectId/platforms/:platformId') +App::put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update platform') ->groups(['api', 'projects']) ->label('scope', 'platforms.write') @@ -1594,7 +1580,7 @@ Http::put('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform->isEmpty()) { + if ($platform === false || $platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1611,7 +1597,7 @@ Http::put('/v1/projects/:projectId/platforms/:platformId') $response->dynamic($platform, Response::MODEL_PLATFORM); }); -Http::delete('/v1/projects/:projectId/platforms/:platformId') +App::delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.delete') @@ -1626,6 +1612,7 @@ Http::delete('/v1/projects/:projectId/platforms/:platformId') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1637,7 +1624,7 @@ Http::delete('/v1/projects/:projectId/platforms/:platformId') Query::equal('projectInternalId', [$project->getInternalId()]), ]); - if ($platform->isEmpty()) { + if ($platform === false || $platform->isEmpty()) { throw new Exception(Exception::PLATFORM_NOT_FOUND); } @@ -1650,7 +1637,7 @@ Http::delete('/v1/projects/:projectId/platforms/:platformId') // CUSTOM SMTP and Templates -Http::patch('/v1/projects/:projectId/smtp') +App::patch('/v1/projects/:projectId/smtp') ->desc('Update SMTP') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1673,6 +1660,7 @@ Http::patch('/v1/projects/:projectId/smtp') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1739,7 +1727,7 @@ Http::patch('/v1/projects/:projectId/smtp') $response->dynamic($project, Response::MODEL_PROJECT); }); -Http::post('/v1/projects/:projectId/smtp/tests') +App::post('/v1/projects/:projectId/smtp/tests') ->desc('Create SMTP test') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1798,7 +1786,7 @@ Http::post('/v1/projects/:projectId/smtp/tests') return $response->noContent(); }); -Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') +App::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Get custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1814,6 +1802,7 @@ Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1823,7 +1812,7 @@ Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { $template = [ @@ -1838,7 +1827,7 @@ Http::get('/v1/projects/:projectId/templates/sms/:type/:locale') }); -Http::get('/v1/projects/:projectId/templates/email/:type/:locale') +App::get('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Get custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1854,6 +1843,7 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1861,7 +1851,7 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; $localeObj = new Locale($locale); if (is_null($template)) { @@ -1869,7 +1859,7 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale') $message ->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello")) ->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer")) - ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escape: false) + ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false) ->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks")) ->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature")) ->setParam('{{direction}}', $localeObj->getText('settings.direction')); @@ -1889,7 +1879,7 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale') $response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE); }); -Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale') +App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Update custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1906,6 +1896,7 @@ Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -1928,7 +1919,7 @@ Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -Http::patch('/v1/projects/:projectId/templates/email/:type/:locale') +App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Update custom email templates') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1949,6 +1940,7 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1977,7 +1969,7 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale') ]), Response::MODEL_EMAIL_TEMPLATE); }); -Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') +App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->desc('Reset custom SMS template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1993,6 +1985,7 @@ Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); $project = $dbForConsole->getDocument('projects', $projectId); @@ -2002,7 +1995,7 @@ Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['sms.' . $type . '-' . $locale] ?? null; + $template = $templates['sms.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); @@ -2019,7 +2012,7 @@ Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ]), Response::MODEL_SMS_TEMPLATE); }); -Http::delete('/v1/projects/:projectId/templates/email/:type/:locale') +App::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->desc('Reset custom email template') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -2035,6 +2028,7 @@ Http::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -2042,7 +2036,7 @@ Http::delete('/v1/projects/:projectId/templates/email/:type/:locale') } $templates = $project->getAttribute('templates', []); - $template = $templates['email.' . $type . '-' . $locale] ?? null; + $template = $templates['email.' . $type . '-' . $locale] ?? null; if (is_null($template)) { throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION); diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index d4283691ae..84484a7209 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -7,6 +7,7 @@ use Appwrite\Extend\Exception; use Appwrite\Network\Validator\CNAME; use Appwrite\Utopia\Database\Validator\Queries\Rules; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; @@ -14,14 +15,13 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; -use Utopia\Http\Http; -use Utopia\Http\Validator\Domain as ValidatorDomain; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Logger\Log; use Utopia\System\System; +use Utopia\Validator\Domain as ValidatorDomain; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; -Http::post('/v1/proxy/rules') +App::post('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('Create rule') ->label('scope', 'rules.write') @@ -63,7 +63,7 @@ Http::post('/v1/proxy/rules') Query::equal('domain', [$domain]), ]); - if (!$document->isEmpty()) { + if ($document && !$document->isEmpty()) { if ($document->getAttribute('projectId') === $project->getId()) { $resourceType = $document->getAttribute('resourceType'); $resourceId = $document->getAttribute('resourceId'); @@ -147,7 +147,7 @@ Http::post('/v1/proxy/rules') ->dynamic($rule, Response::MODEL_PROXY_RULE); }); -Http::get('/v1/proxy/rules') +App::get('/v1/proxy/rules') ->groups(['api', 'proxy']) ->desc('List rules') ->label('scope', 'rules.read') @@ -210,7 +210,7 @@ Http::get('/v1/proxy/rules') ]), Response::MODEL_PROXY_RULE_LIST); }); -Http::get('/v1/proxy/rules/:ruleId') +App::get('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Get rule') ->label('scope', 'rules.read') @@ -239,7 +239,7 @@ Http::get('/v1/proxy/rules/:ruleId') $response->dynamic($rule, Response::MODEL_PROXY_RULE); }); -Http::delete('/v1/proxy/rules/:ruleId') +App::delete('/v1/proxy/rules/:ruleId') ->groups(['api', 'proxy']) ->desc('Delete rule') ->label('scope', 'rules.write') @@ -276,8 +276,8 @@ Http::delete('/v1/proxy/rules/:ruleId') $response->noContent(); }); -Http::patch('/v1/proxy/rules/:ruleId/verification') - ->desc('Update Rule Verification Status') +App::patch('/v1/proxy/rules/:ruleId/verification') + ->desc('Update rule verification status') ->groups(['api', 'proxy']) ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].update') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index d7357ada19..4e30832a67 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -11,8 +11,8 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Buckets; use Appwrite\Utopia\Database\Validator\Queries\Files; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -23,16 +23,8 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\HexColor; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Image\Image; use Utopia\Storage\Compression\Algorithms\GZIP; use Utopia\Storage\Compression\Algorithms\Zstd; @@ -43,9 +35,16 @@ use Utopia\Storage\Validator\File; use Utopia\Storage\Validator\FileExt; use Utopia\Storage\Validator\FileSize; use Utopia\Storage\Validator\Upload; +use Utopia\Swoole\Request; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Boolean; +use Utopia\Validator\HexColor; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; -Http::post('/v1/storage/buckets') +App::post('/v1/storage/buckets') ->desc('Create bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -143,7 +142,7 @@ Http::post('/v1/storage/buckets') ->dynamic($bucket, Response::MODEL_BUCKET); }); -Http::get('/v1/storage/buckets') +App::get('/v1/storage/buckets') ->desc('List buckets') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -197,7 +196,7 @@ Http::get('/v1/storage/buckets') ]), Response::MODEL_BUCKET_LIST); }); -Http::get('/v1/storage/buckets/:bucketId') +App::get('/v1/storage/buckets/:bucketId') ->desc('Get bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.read') @@ -222,7 +221,7 @@ Http::get('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -Http::put('/v1/storage/buckets/:bucketId') +App::put('/v1/storage/buckets/:bucketId') ->desc('Update bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -289,7 +288,7 @@ Http::put('/v1/storage/buckets/:bucketId') $response->dynamic($bucket, Response::MODEL_BUCKET); }); -Http::delete('/v1/storage/buckets/:bucketId') +App::delete('/v1/storage/buckets/:bucketId') ->desc('Delete bucket') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') @@ -330,7 +329,7 @@ Http::delete('/v1/storage/buckets/:bucketId') $response->noContent(); }); -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('Create file') ->groups(['api', 'storage']) @@ -362,19 +361,19 @@ Http::post('/v1/storage/buckets/:bucketId/files') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { + $validator = new Authorization(Database::PERMISSION_CREATE); + if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -398,7 +397,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') } // Users can only manage their own roles, API keys and Admin users can manage any - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -411,7 +410,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!$authorization->isRole($role)) { + if (!Authorization::isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -631,10 +630,11 @@ Http::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { + $validator = new Authorization(Database::PERMISSION_CREATE); + if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } else { if ($file->isEmpty()) { @@ -669,10 +669,11 @@ Http::post('/v1/storage/buckets/:bucketId/files') * However as with chunk upload even if we are updating, we are essentially creating a file * adding it's new chunk so we validate create permission instead of update */ - if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) { + $validator = new Authorization(Database::PERMISSION_CREATE); + if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } } @@ -689,7 +690,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') ->dynamic($file, Response::MODEL_FILE); }); -Http::get('/v1/storage/buckets/:bucketId/files') +App::get('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ->desc('List files') ->groups(['api', 'storage']) @@ -707,19 +708,19 @@ Http::get('/v1/storage/buckets/:bucketId/files') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -748,7 +749,7 @@ Http::get('/v1/storage/buckets/:bucketId/files') if ($fileSecurity && !$valid) { $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($cursorDocument->isEmpty()) { @@ -764,8 +765,8 @@ Http::get('/v1/storage/buckets/:bucketId/files') $files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries); $total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { - $files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); - $total = $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); + $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries)); + $total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); } $response->dynamic(new Document([ @@ -774,7 +775,7 @@ Http::get('/v1/storage/buckets/:bucketId/files') ]), Response::MODEL_FILE_LIST); }); -Http::get('/v1/storage/buckets/:bucketId/files/:fileId') +App::get('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Get file') ->groups(['api', 'storage']) @@ -791,19 +792,19 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode, Authorization $authorization) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) { + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -811,7 +812,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -821,7 +822,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->alias('/v1/storage/files/:fileId/preview', ['bucketId' => 'default']) ->desc('Get file preview') ->groups(['api', 'storage']) @@ -856,24 +857,24 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) { + ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); } - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -881,7 +882,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -993,7 +994,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') unset($image); }); -Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') +App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->alias('/v1/storage/files/:fileId/download', ['bucketId' => 'default']) ->desc('Get file for download') ->groups(['api', 'storage']) @@ -1012,20 +1013,20 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { + ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1033,7 +1034,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1133,7 +1134,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } }); -Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') +App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->alias('/v1/storage/files/:fileId/view', ['bucketId' => 'default']) ->desc('Get file for view') ->groups(['api', 'storage']) @@ -1152,19 +1153,19 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('dbForProject') ->inject('mode') ->inject('deviceForFiles') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles, Authorization $authorization) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) { + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1172,7 +1173,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -1285,7 +1286,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/view') } }); -Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') +App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->desc('Get file for push notification') ->groups(['api', 'storage']) ->label('scope', 'public') @@ -1301,9 +1302,8 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->inject('project') ->inject('mode') ->inject('deviceForFiles') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles, Authorization $authorization) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) { + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); @@ -1321,14 +1321,14 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') throw new Exception(Exception::USER_UNAUTHORIZED); } - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1439,7 +1439,7 @@ Http::get('/v1/storage/buckets/:bucketId/files/:fileId/push') } }); -Http::put('/v1/storage/buckets/:bucketId/files/:fileId') +App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->alias('/v1/storage/files/:fileId', ['bucketId' => 'default']) ->desc('Update file') ->groups(['api', 'storage']) @@ -1466,26 +1466,26 @@ Http::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('user') ->inject('mode') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate())); + $validator = new Authorization(Database::PERMISSION_UPDATE); + $valid = $validator->isValid($bucket->getUpdate()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for update - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); @@ -1499,7 +1499,7 @@ Http::put('/v1/storage/buckets/:bucketId/files/:fileId') ]); // Users can only manage their own roles, API keys and Admin users can manage any - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { @@ -1512,7 +1512,7 @@ Http::put('/v1/storage/buckets/:bucketId/files/:fileId') $permission->getIdentifier(), $permission->getDimension() ))->toString(); - if (!$authorization->isRole($role)) { + if (!Authorization::isRole($role)) { throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } @@ -1532,7 +1532,7 @@ Http::put('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file); } else { - $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); + $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); } $queueForEvents @@ -1544,8 +1544,8 @@ Http::put('/v1/storage/buckets/:bucketId/files/:fileId') $response->dynamic($file, Response::MODEL_FILE); }); -Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') - ->desc('Delete File') +App::delete('/v1/storage/buckets/:bucketId/files/:fileId') + ->desc('Delete file') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].delete') @@ -1568,32 +1568,32 @@ Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('mode') ->inject('deviceForFiles') ->inject('queueForDeletes') - ->inject('authorization') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes, Authorization $authorization) { - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) { + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $bucket->getDelete())); + $validator = new Authorization(Database::PERMISSION_DELETE); + $valid = $validator->isValid($bucket->getDelete()); if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } // Read permission should not be required for delete - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } // Make sure we don't delete the file before the document permission check occurs - if ($fileSecurity && !$valid && !$authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()))) { + if ($fileSecurity && !$valid && !$validator->isValid($file->getDelete())) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -1617,7 +1617,7 @@ Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') if ($fileSecurity && !$valid) { $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if (!$deleted) { @@ -1637,7 +1637,7 @@ Http::delete('/v1/storage/buckets/:bucketId/files/:fileId') $response->noContent(); }); -Http::get('/v1/storage/usage') +App::get('/v1/storage/usage') ->desc('Get storage usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1650,8 +1650,7 @@ Http::get('/v1/storage/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $range, Response $response, Database $dbForProject) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -1663,7 +1662,7 @@ Http::get('/v1/storage/usage') ]; $total = []; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), @@ -1717,7 +1716,7 @@ Http::get('/v1/storage/usage') ]), Response::MODEL_USAGE_STORAGE); }); -Http::get('/v1/storage/:bucketId/usage') +App::get('/v1/storage/:bucketId/usage') ->desc('Get bucket usage stats') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -1731,8 +1730,7 @@ Http::get('/v1/storage/:bucketId/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) { $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -1748,7 +1746,8 @@ Http::get('/v1/storage/:bucketId/usage') str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE), ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { + + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 8f5fba30ab..146b5d5f81 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,7 +1,6 @@ desc('Create team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].create') @@ -67,16 +66,15 @@ Http::post('/v1/teams') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAppUser = Auth::isAppUser(Authorization::getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; try { - $team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([ + $team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([ '$id' => $teamId, '$permissions' => [ Permission::read(Role::team($teamId)), @@ -135,7 +133,7 @@ Http::post('/v1/teams') ->dynamic($team, Response::MODEL_TEAM); }); -Http::get('/v1/teams') +App::get('/v1/teams') ->desc('List teams') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -193,7 +191,7 @@ Http::get('/v1/teams') ]), Response::MODEL_TEAM_LIST); }); -Http::get('/v1/teams/:teamId') +App::get('/v1/teams/:teamId') ->desc('Get team') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -220,7 +218,7 @@ Http::get('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -Http::get('/v1/teams/:teamId/prefs') +App::get('/v1/teams/:teamId/prefs') ->desc('Get team preferences') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -248,7 +246,7 @@ Http::get('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -Http::put('/v1/teams/:teamId') +App::put('/v1/teams/:teamId') ->desc('Update name') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update') @@ -291,7 +289,7 @@ Http::put('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); -Http::put('/v1/teams/:teamId/prefs') +App::put('/v1/teams/:teamId/prefs') ->desc('Update preferences') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update.prefs') @@ -327,7 +325,7 @@ Http::put('/v1/teams/:teamId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -Http::delete('/v1/teams/:teamId') +App::delete('/v1/teams/:teamId') ->desc('Delete team') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].delete') @@ -376,7 +374,7 @@ Http::delete('/v1/teams/:teamId') $response->noContent(); }); -Http::post('/v1/teams/:teamId/memberships') +App::post('/v1/teams/:teamId/memberships') ->desc('Create team membership') ->groups(['api', 'teams', 'auth']) ->label('event', 'teams.[teamId].memberships.[membershipId].create') @@ -418,10 +416,9 @@ Http::post('/v1/teams/:teamId/memberships') ->inject('queueForMails') ->inject('queueForMessaging') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, Authorization $authorization) { - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) { + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -433,8 +430,8 @@ Http::post('/v1/teams/:teamId/memberships') if (empty($userId) && empty($email) && empty($phone)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); } - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAppUser = Auth::isAppUser(Authorization::getRoles()); if (!$isPrivilegedUser && !$isAppUser && empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED); @@ -463,17 +460,17 @@ Http::post('/v1/teams/:teamId/memberships') $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - if ($invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { + if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if ($invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') !== $email) { + if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') !== $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); } } - if (!isset($invitee) || $invitee->isEmpty()) { // Create new user if no user with same email found + if (empty($invitee)) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed. @@ -494,7 +491,7 @@ Http::post('/v1/teams/:teamId/memberships') try { $userId = ID::unique(); - $invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([ + $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, '$permissions' => [ Permission::read(Role::any()), @@ -525,13 +522,12 @@ Http::post('/v1/teams/:teamId/memberships') 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), ]))); - } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } } - $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); + $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team'); @@ -563,12 +559,12 @@ Http::post('/v1/teams/:teamId/memberships') if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership try { - $membership = $authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)); + $membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } - $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { @@ -590,7 +586,7 @@ Http::post('/v1/teams/:teamId/memberships') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message - ->setParam('{{body}}', $body, escape: false) + ->setParam('{{body}}', $body, escapeHtml: false) ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) @@ -708,7 +704,7 @@ Http::post('/v1/teams/:teamId/memberships') ); }); -Http::get('/v1/teams/:teamId/memberships') +App::get('/v1/teams/:teamId/memberships') ->desc('List team memberships') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -811,7 +807,7 @@ Http::get('/v1/teams/:teamId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -Http::get('/v1/teams/:teamId/memberships/:membershipId') +App::get('/v1/teams/:teamId/memberships/:membershipId') ->desc('Get team membership') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -867,7 +863,7 @@ Http::get('/v1/teams/:teamId/memberships/:membershipId') $response->dynamic($membership, Response::MODEL_MEMBERSHIP); }); -Http::patch('/v1/teams/:teamId/memberships/:membershipId') +App::patch('/v1/teams/:teamId/memberships/:membershipId') ->desc('Update membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update') @@ -899,8 +895,7 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -917,9 +912,9 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); - $isOwner = $authorization->isRole('team:' . $team->getId() . '/owner'); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles'); @@ -950,7 +945,7 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId') ); }); -Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') +App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->desc('Update team membership status') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update.status') @@ -976,9 +971,7 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('project') ->inject('geodb') ->inject('queueForEvents') - ->inject('authorization') - ->inject('authentication') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Authorization $authorization, Authentication $authentication) { + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -987,7 +980,7 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') throw new Exception(Exception::MEMBERSHIP_NOT_FOUND); } - $team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId)); + $team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId)); if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); @@ -1022,11 +1015,11 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->setAttribute('confirm', true) ; - $authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); + Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true))); // Log user in - $authorization->addRole(Role::user($user->getId())->toString()); + Authorization::setRole(Role::user($user->getId())->toString()); $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); @@ -1056,13 +1049,13 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') $dbForProject->purgeCachedDocument('users', $user->getId()); - $authorization->addRole(Role::user($userId)->toString()); + Authorization::setRole(Role::user($userId)->toString()); $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); $dbForProject->purgeCachedDocument('users', $user->getId()); - $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); $queueForEvents ->setParam('userId', $user->getId()) @@ -1072,13 +1065,13 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', \json_encode([$authentication->getCookieName() => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } $response - ->addCookie($authentication->getCookieName() . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie($authentication->getCookieName(), Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic( @@ -1090,7 +1083,7 @@ Http::patch('/v1/teams/:teamId/memberships/:membershipId/status') ); }); -Http::delete('/v1/teams/:teamId/memberships/:membershipId') +App::delete('/v1/teams/:teamId/memberships/:membershipId') ->desc('Delete team membership') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].delete') @@ -1108,8 +1101,7 @@ Http::delete('/v1/teams/:teamId/memberships/:membershipId') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('authorization') - ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization) { + ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1144,7 +1136,7 @@ Http::delete('/v1/teams/:teamId/memberships/:membershipId') $dbForProject->purgeCachedDocument('users', $user->getId()); if ($membership->getAttribute('confirm')) { // Count only confirmed members - $authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); + Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); } $queueForEvents @@ -1157,7 +1149,7 @@ Http::delete('/v1/teams/:teamId/memberships/:membershipId') $response->noContent(); }); -Http::get('/v1/teams/:teamId/logs') +App::get('/v1/teams/:teamId/logs') ->desc('List team logs') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -1174,8 +1166,7 @@ Http::get('/v1/teams/:teamId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $team = $dbForProject->getDocument('teams', $teamId); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9f33ee38db..571df4fdb2 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -22,6 +22,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Users; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Audit\Audit; use Utopia\Config\Config; use Utopia\Database\Database; @@ -38,16 +39,15 @@ use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Assoc; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Locale\Locale; use Utopia\System\System; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Assoc; +use Utopia\Validator\Boolean; +use Utopia\Validator\Integer; +use Utopia\Validator\Range; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; /** TODO: Remove function when we move to using utopia/platform */ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document @@ -63,7 +63,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $identityWithMatchingEmail = $dbForProject->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } } @@ -140,7 +140,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), ]); - if (!$existingTarget->isEmpty()) { + if ($existingTarget) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -164,7 +164,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e $existingTarget = $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), ]); - if (!$existingTarget->isEmpty()) { + if ($existingTarget) { $user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND); } } @@ -180,7 +180,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e return $user; } -Http::post('/v1/users') +App::post('/v1/users') ->desc('Create user') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -211,7 +211,7 @@ Http::post('/v1/users') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/bcrypt') +App::post('/v1/users/bcrypt') ->desc('Create user with bcrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -242,7 +242,7 @@ Http::post('/v1/users/bcrypt') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/md5') +App::post('/v1/users/md5') ->desc('Create user with MD5 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -273,7 +273,7 @@ Http::post('/v1/users/md5') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/argon2') +App::post('/v1/users/argon2') ->desc('Create user with Argon2 password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -304,7 +304,7 @@ Http::post('/v1/users/argon2') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/sha') +App::post('/v1/users/sha') ->desc('Create user with SHA password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -342,7 +342,7 @@ Http::post('/v1/users/sha') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/phpass') +App::post('/v1/users/phpass') ->desc('Create user with PHPass password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -373,7 +373,7 @@ Http::post('/v1/users/phpass') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/scrypt') +App::post('/v1/users/scrypt') ->desc('Create user with Scrypt password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -417,7 +417,7 @@ Http::post('/v1/users/scrypt') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/scrypt-modified') +App::post('/v1/users/scrypt-modified') ->desc('Create user with Scrypt modified password') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') @@ -451,7 +451,7 @@ Http::post('/v1/users/scrypt-modified') ->dynamic($user, Response::MODEL_USER); }); -Http::post('/v1/users/:userId/targets') +App::post('/v1/users/:userId/targets') ->desc('Create user target') ->groups(['api', 'users']) ->label('audits.event', 'target.create') @@ -540,7 +540,7 @@ Http::post('/v1/users/:userId/targets') ->dynamic($target, Response::MODEL_TARGET); }); -Http::get('/v1/users') +App::get('/v1/users') ->desc('List users') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -594,7 +594,7 @@ Http::get('/v1/users') ]), Response::MODEL_USER_LIST); }); -Http::get('/v1/users/:userId') +App::get('/v1/users/:userId') ->desc('Get user') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -619,7 +619,7 @@ Http::get('/v1/users/:userId') $response->dynamic($user, Response::MODEL_USER); }); -Http::get('/v1/users/:userId/prefs') +App::get('/v1/users/:userId/prefs') ->desc('Get user preferences') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -646,7 +646,7 @@ Http::get('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -Http::get('/v1/users/:userId/targets/:targetId') +App::get('/v1/users/:userId/targets/:targetId') ->desc('Get user target') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -678,7 +678,7 @@ Http::get('/v1/users/:userId/targets/:targetId') $response->dynamic($target, Response::MODEL_TARGET); }); -Http::get('/v1/users/:userId/sessions') +App::get('/v1/users/:userId/sessions') ->desc('List user sessions') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -719,7 +719,7 @@ Http::get('/v1/users/:userId/sessions') ]), Response::MODEL_SESSION_LIST); }); -Http::get('/v1/users/:userId/memberships') +App::get('/v1/users/:userId/memberships') ->desc('List user memberships') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -758,7 +758,7 @@ Http::get('/v1/users/:userId/memberships') ]), Response::MODEL_MEMBERSHIP_LIST); }); -Http::get('/v1/users/:userId/logs') +App::get('/v1/users/:userId/logs') ->desc('List user logs') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -775,8 +775,7 @@ Http::get('/v1/users/:userId/logs') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('authorization') - ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization) { + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $user = $dbForProject->getDocument('users', $userId); @@ -848,7 +847,7 @@ Http::get('/v1/users/:userId/logs') ]), Response::MODEL_LOG_LIST); }); -Http::get('/v1/users/:userId/targets') +App::get('/v1/users/:userId/targets') ->desc('List user targets') ->groups(['api', 'users']) ->label('scope', 'targets.read') @@ -903,7 +902,7 @@ Http::get('/v1/users/:userId/targets') ]), Response::MODEL_TARGET_LIST); }); -Http::get('/v1/users/identities') +App::get('/v1/users/identities') ->desc('List identities') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -957,7 +956,7 @@ Http::get('/v1/users/identities') ]), Response::MODEL_IDENTITY_LIST); }); -Http::patch('/v1/users/:userId/status') +App::patch('/v1/users/:userId/status') ->desc('Update user status') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.status') @@ -993,7 +992,7 @@ Http::patch('/v1/users/:userId/status') $response->dynamic($user, Response::MODEL_USER); }); -Http::put('/v1/users/:userId/labels') +App::put('/v1/users/:userId/labels') ->desc('Update user labels') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.labels') @@ -1030,7 +1029,7 @@ Http::put('/v1/users/:userId/labels') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/verification/phone') +App::patch('/v1/users/:userId/verification/phone') ->desc('Update phone verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1065,7 +1064,7 @@ Http::patch('/v1/users/:userId/verification/phone') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/name') +App::patch('/v1/users/:userId/name') ->desc('Update name') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.name') @@ -1102,7 +1101,7 @@ Http::patch('/v1/users/:userId/name') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/password') +App::patch('/v1/users/:userId/password') ->desc('Update password') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.password') @@ -1179,7 +1178,7 @@ Http::patch('/v1/users/:userId/password') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/email') +App::patch('/v1/users/:userId/email') ->desc('Update email') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.email') @@ -1215,7 +1214,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('providerEmail', [$email]), Query::notEqual('userInternalId', $user->getInternalId()), ]); - if (!$identityWithMatchingEmail->isEmpty()) { + if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1223,7 +1222,7 @@ Http::patch('/v1/users/:userId/email') Query::equal('identifier', [$email]), ]); - if (!$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } @@ -1274,7 +1273,7 @@ Http::patch('/v1/users/:userId/email') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/phone') +App::patch('/v1/users/:userId/phone') ->desc('Update phone') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.phone') @@ -1313,7 +1312,7 @@ Http::patch('/v1/users/:userId/phone') Query::equal('identifier', [$number]), ]); - if (!$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } } @@ -1357,7 +1356,7 @@ Http::patch('/v1/users/:userId/phone') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/verification') +App::patch('/v1/users/:userId/verification') ->desc('Update email verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') @@ -1392,7 +1391,7 @@ Http::patch('/v1/users/:userId/verification') $response->dynamic($user, Response::MODEL_USER); }); -Http::patch('/v1/users/:userId/prefs') +App::patch('/v1/users/:userId/prefs') ->desc('Update user preferences') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.prefs') @@ -1425,7 +1424,7 @@ Http::patch('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -Http::patch('/v1/users/:userId/targets/:targetId') +App::patch('/v1/users/:userId/targets/:targetId') ->desc('Update user target') ->groups(['api', 'users']) ->label('audits.event', 'target.update') @@ -1519,7 +1518,7 @@ Http::patch('/v1/users/:userId/targets/:targetId') ->dynamic($target, Response::MODEL_TARGET); }); -Http::patch('/v1/users/:userId/mfa') +App::patch('/v1/users/:userId/mfa') ->desc('Update MFA') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa') @@ -1557,7 +1556,7 @@ Http::patch('/v1/users/:userId/mfa') $response->dynamic($user, Response::MODEL_USER); }); -Http::get('/v1/users/:userId/mfa/factors') +App::get('/v1/users/:userId/mfa/factors') ->desc('List factors') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1590,7 +1589,7 @@ Http::get('/v1/users/:userId/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -Http::get('/v1/users/:userId/mfa/recovery-codes') +App::get('/v1/users/:userId/mfa/recovery-codes') ->desc('Get MFA recovery codes') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -1625,7 +1624,7 @@ Http::get('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::patch('/v1/users/:userId/mfa/recovery-codes') +App::patch('/v1/users/:userId/mfa/recovery-codes') ->desc('Create MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].create.mfa.recovery-codes') @@ -1671,7 +1670,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::put('/v1/users/:userId/mfa/recovery-codes') +App::put('/v1/users/:userId/mfa/recovery-codes') ->desc('Regenerate MFA recovery codes') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.mfa.recovery-codes') @@ -1716,7 +1715,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes') $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); }); -Http::delete('/v1/users/:userId/mfa/authenticators/:type') +App::delete('/v1/users/:userId/mfa/authenticators/:type') ->desc('Delete authenticator') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete.mfa') @@ -1758,7 +1757,7 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type') $response->noContent(); }); -Http::post('/v1/users/:userId/sessions') +App::post('/v1/users/:userId/sessions') ->desc('Create session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -1828,7 +1827,7 @@ Http::post('/v1/users/:userId/sessions') ->dynamic($session, Response::MODEL_SESSION); }); -Http::post('/v1/users/:userId/tokens') +App::post('/v1/users/:userId/tokens') ->desc('Create token') ->groups(['api', 'users']) ->label('event', 'users.[userId].tokens.[tokenId].create') @@ -1885,7 +1884,7 @@ Http::post('/v1/users/:userId/tokens') ->dynamic($token, Response::MODEL_TOKEN); }); -Http::delete('/v1/users/:userId/sessions/:sessionId') +App::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete user session') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].delete') @@ -1928,7 +1927,7 @@ Http::delete('/v1/users/:userId/sessions/:sessionId') $response->noContent(); }); -Http::delete('/v1/users/:userId/sessions') +App::delete('/v1/users/:userId/sessions') ->desc('Delete user sessions') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.delete') @@ -1970,7 +1969,7 @@ Http::delete('/v1/users/:userId/sessions') $response->noContent(); }); -Http::delete('/v1/users/:userId') +App::delete('/v1/users/:userId') ->desc('Delete user') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete') @@ -2012,7 +2011,7 @@ Http::delete('/v1/users/:userId') $response->noContent(); }); -Http::delete('/v1/users/:userId/targets/:targetId') +App::delete('/v1/users/:userId/targets/:targetId') ->desc('Delete user target') ->groups(['api', 'users']) ->label('audits.event', 'target.delete') @@ -2063,7 +2062,7 @@ Http::delete('/v1/users/:userId/targets/:targetId') $response->noContent(); }); -Http::delete('/v1/users/identities/:identityId') +App::delete('/v1/users/identities/:identityId') ->desc('Delete identity') ->groups(['api', 'users']) ->label('event', 'users.[userId].identities.[identityId].delete') @@ -2098,7 +2097,7 @@ Http::delete('/v1/users/identities/:identityId') return $response->noContent(); }); -Http::post('/v1/users/:userId/jwts') +App::post('/v1/users/:userId/jwts') ->desc('Create user JWT') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -2148,7 +2147,7 @@ Http::post('/v1/users/:userId/jwts') ])]), Response::MODEL_JWT); }); -Http::get('/v1/users/usage') +App::get('/v1/users/usage') ->desc('Get users usage stats') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -2161,8 +2160,8 @@ Http::get('/v1/users/usage') ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) ->inject('response') ->inject('dbForProject') - ->inject('authorization') - ->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) { + ->inject('register') + ->action(function (string $range, Response $response, Database $dbForProject) { $periods = Config::getParam('usage', []); $stats = $usage = []; @@ -2172,7 +2171,7 @@ Http::get('/v1/users/usage') METRIC_SESSIONS, ]; - $authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) { + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $count => $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 1f97e4f2d5..f3381490ec 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -8,6 +8,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; +use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -31,17 +32,17 @@ use Utopia\Detector\Adapter\Python; use Utopia\Detector\Adapter\Ruby; use Utopia\Detector\Adapter\Swift; use Utopia\Detector\Detector; -use Utopia\Http\Http; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Host; -use Utopia\Http\Validator\Text; use Utopia\System\System; +use Utopia\Validator\Boolean; +use Utopia\Validator\Host; +use Utopia\Validator\Text; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; use function Swoole\Coroutine\batch; -$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request, Authorization $auth) { +$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) { + $errors = []; foreach ($repositories as $resource) { try { $resourceType = $resource->getAttribute('resourceType'); @@ -51,11 +52,11 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } $projectId = $resource->getAttribute('projectId'); - $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); $functionId = $resource->getAttribute('resourceId'); - $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); $functionInternalId = $function->getInternalId(); $deploymentId = ID::unique(); @@ -101,14 +102,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $latestCommentId = ''; - if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false)) { - $latestComment = $auth->skip(fn () => $dbForConsole->findOne('vcsComments', [ + if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) { + $latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerPullRequestId', [$providerPullRequestId]), Query::orderDesc('$createdAt'), ])); - if (!$latestComment->isEmpty()) { + if ($latestComment !== false && !$latestComment->isEmpty()) { $latestCommentId = $latestComment->getAttribute('providerCommentId', ''); $comment = new Comment(); $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); @@ -123,7 +124,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId if (!empty($latestCommentId)) { $teamId = $project->getAttribute('teamId', ''); - $latestComment = $auth->skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ + $latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::team(ID::custom($teamId))), @@ -144,7 +145,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } } } elseif (!empty($providerBranch)) { - $latestComments = $auth->skip(fn () => $dbForConsole->find('vcsComments', [ + $latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::equal('providerBranch', [$providerBranch]), Query::orderDesc('$createdAt'), @@ -261,8 +262,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } }; -Http::get('/v1/vcs/github/authorize') - ->desc('Install GitHub App') +App::get('/v1/vcs/github/authorize') + ->desc('Install GitHub app') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -303,8 +304,8 @@ Http::get('/v1/vcs/github/authorize') ->redirect($url); }); -Http::get('/v1/vcs/github/callback') - ->desc('Capture installation and authorization from GitHub App') +App::get('/v1/vcs/github/callback') + ->desc('Capture installation and authorization from GitHub app') ->groups(['api', 'vcs']) ->label('scope', 'public') ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -369,7 +370,7 @@ Http::get('/v1/vcs/github/callback') $identity = $dbForConsole->findOne('identities', [ Query::equal('providerEmail', [$email]), ]); - if (!$identity->isEmpty()) { + if ($identity !== false && !$identity->isEmpty()) { if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -416,7 +417,7 @@ Http::get('/v1/vcs/github/callback') Query::equal('projectInternalId', [$projectInternalId]) ]); - if ($installation->isEmpty()) { + if ($installation === false || $installation->isEmpty()) { $teamId = $project->getAttribute('teamId', ''); $installation = new Document([ @@ -463,7 +464,7 @@ Http::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') +App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') ->desc('Get files and directories of a VCS repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -524,7 +525,7 @@ Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pr ]), Response::MODEL_VCS_CONTENT_LIST); }); -Http::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') +App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') ->desc('Detect runtime settings from source code') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -596,8 +597,8 @@ Http::post('/v1/vcs/github/installations/:installationId/providerRepositories/:p $response->dynamic(new Document($detection), Response::MODEL_DETECTION); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('List Repositories') +App::get('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('List repositories') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -691,7 +692,7 @@ Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') ]), Response::MODEL_PROVIDER_REPOSITORY_LIST); }); -Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') +App::post('/v1/vcs/github/installations/:installationId/providerRepositories') ->desc('Create repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -724,7 +725,7 @@ Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') Query::equal('provider', ['github']), Query::equal('userInternalId', [$user->getInternalId()]), ]); - if ($identity->isEmpty()) { + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -792,7 +793,7 @@ Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') +App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') ->desc('Get repository') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -841,8 +842,8 @@ Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pr $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') - ->desc('List Repository Branches') +App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') + ->desc('List repository branches') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') ->label('sdk.namespace', 'vcs') @@ -890,8 +891,8 @@ Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pr ]), Response::MODEL_BRANCH_LIST); }); -Http::post('/v1/vcs/github/events') - ->desc('Create Event') +App::post('/v1/vcs/github/events') + ->desc('Create event') ->groups(['api', 'vcs']) ->label('scope', 'public') ->inject('gitHub') @@ -900,9 +901,8 @@ Http::post('/v1/vcs/github/events') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('authorization') ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { + function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { $payload = $request->getRawPayload(); $signatureRemote = $request->getHeader('x-hub-signature-256', ''); $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); @@ -936,14 +936,14 @@ Http::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request); } } elseif ($event == $github::EVENT_INSTALLATION) { if ($parsedPayload["action"] == "deleted") { @@ -956,13 +956,13 @@ Http::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) ])); foreach ($repositories as $repository) { - $authorization->skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); + Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -994,12 +994,12 @@ Http::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request); } elseif ($parsedPayload["action"] == "closed") { // Allowed external contributions cleanup @@ -1008,7 +1008,7 @@ Http::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = $authorization->skip(fn () => $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); @@ -1019,7 +1019,7 @@ Http::post('/v1/vcs/github/events') if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); } } } @@ -1030,7 +1030,7 @@ Http::post('/v1/vcs/github/events') } ); -Http::get('/v1/vcs/installations') +App::get('/v1/vcs/installations') ->desc('List installations') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1090,7 +1090,7 @@ Http::get('/v1/vcs/installations') ]), Response::MODEL_INSTALLATION_LIST); }); -Http::get('/v1/vcs/installations/:installationId') +App::get('/v1/vcs/installations/:installationId') ->desc('Get installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.read') @@ -1119,8 +1119,8 @@ Http::get('/v1/vcs/installations/:installationId') $response->dynamic($installation, Response::MODEL_INSTALLATION); }); -Http::delete('/v1/vcs/installations/:installationId') - ->desc('Delete Installation') +App::delete('/v1/vcs/installations/:installationId') + ->desc('Delete installation') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') ->label('sdk.namespace', 'vcs') @@ -1152,7 +1152,7 @@ Http::delete('/v1/vcs/installations/:installationId') $response->noContent(); }); -Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') +App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') ->desc('Authorize external deployment') ->groups(['api', 'vcs']) ->label('scope', 'vcs.write') @@ -1172,15 +1172,14 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito ->inject('dbForConsole') ->inject('getProjectDB') ->inject('queueForBuilds') - ->inject('authorization') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds, Authorization $authorization) use ($createGitDeployments) { + ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = $authorization->skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) ])); @@ -1197,7 +1196,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito // TODO: Delete from array when PR is closed - $repository = $authorization->skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); + $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); @@ -1221,7 +1220,7 @@ Http::patch('/v1/vcs/github/installations/:installationId/repositories/:reposito $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request, $authorization); + $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request); $response->noContent(); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 70cf55d280..04554a940e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1,5 +1,7 @@ label('error', __DIR__ . '/../views/general/error.phtml'); + $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); $host = $request->getHostname() ?? ''; - $rule = $auth->skip( + $route = Authorization::skip( fn () => $dbForConsole->find('rules', [ Query::equal('domain', [$host]), Query::limit(1) ]) )[0] ?? null; - if ($rule === null) { + if ($route === null) { if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) { throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.'); } @@ -72,12 +73,12 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request } // Act as API - no Proxy logic - $route?->label('error', ''); + $utopia->getRoute()?->label('error', ''); return false; } - $projectId = $rule->getAttribute('projectId'); - $project = $auth->skip( + $projectId = $route->getAttribute('projectId'); + $project = Authorization::skip( fn () => $dbForConsole->getDocument('projects', $projectId) ); if (array_key_exists('proxy', $project->getAttribute('services', []))) { @@ -88,16 +89,16 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request } // Skip Appwrite Router for ACME challenge. Nessessary for certificate generation - $path = ($request->getURI() ?? '/'); + $path = ($swooleRequest->server['request_uri'] ?? '/'); if (\str_starts_with($path, '/.well-known/acme-challenge')) { return false; } - $type = $rule->getAttribute('resourceType'); + $type = $route->getAttribute('resourceType'); if ($type === 'function') { - $route->label('sdk.namespace', 'functions'); - $route->label('sdk.method', 'createExecution'); + $utopia->getRoute()?->label('sdk.namespace', 'functions'); + $utopia->getRoute()?->label('sdk.method', 'createExecution'); if (System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if ($request->getProtocol() !== 'https') { @@ -109,25 +110,26 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request } } - $functionId = $rule->getAttribute('resourceId'); - $projectId = $rule->getAttribute('projectId'); + $functionId = $route->getAttribute('resourceId'); + $projectId = $route->getAttribute('projectId'); - $path = ($request->getURI() ?? '/'); - $query = ($request->getQueryString() ?? ''); + $path = ($swooleRequest->server['request_uri'] ?? '/'); + $query = ($swooleRequest->server['query_string'] ?? ''); if (!empty($query)) { $path .= '?' . $query; } - $body = $request->getRawPayload() ?? ''; - $method = $request->getMethod(); + + $body = $swooleRequest->getContent() ?? ''; + $method = $swooleRequest->server['request_method']; $requestHeaders = $request->getHeaders(); - $project = $auth->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); $dbForProject = $getProjectDB($project); - $function = $auth->skip(fn () => $dbForProject->getDocument('functions', $functionId)); + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); if ($function->isEmpty() || !$function->getAttribute('enabled')) { throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); @@ -143,7 +145,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } - $deployment = $auth->skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); @@ -154,7 +156,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request } /** Check if build has completed */ - $build = $auth->skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); } @@ -215,7 +217,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request 'deploymentInternalId' => $deployment->getInternalId(), 'deploymentId' => $deployment->getId(), 'trigger' => 'http', // http / schedule / event - 'status' => 'processing', // waiting / processing / completed / failed + 'status' => 'processing', // waiting / processing / completed / failed 'responseStatusCode' => 0, 'responseHeaders' => [], 'requestPath' => $path, @@ -311,6 +313,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, logging: $function->getAttribute('logging', true), + requestTimeout: 30 ); $headersFiltered = []; @@ -328,6 +331,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('duration', $executionResponse['duration']); + } catch (\Throwable $th) { $durationEnd = \microtime(true); @@ -362,8 +366,7 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request ->trigger() ; - /** @var Document $execution */ - $execution = $auth->skip(fn () => $dbForProject->createDocument('executions', $execution)); + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } $execution->setAttribute('logs', ''); @@ -395,15 +398,18 @@ function router(Database $dbForConsole, callable $getProjectDB, Request $request return true; } elseif ($type === 'api') { - $route?->label('error', ''); + $utopia->getRoute()?->label('error', ''); return false; } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); } + + $utopia->getRoute()?->label('error', ''); + return false; } /* -Http::init() +App::init() ->groups(['api']) ->inject('project') ->inject('mode') @@ -414,7 +420,7 @@ Http::init() }); */ -Http::init() +App::init() ->groups(['database', 'functions', 'storage', 'messaging']) ->inject('project') ->inject('request') @@ -427,11 +433,12 @@ Http::init() } }); -Http::init() +App::init() ->groups(['api', 'web']) + ->inject('utopia') + ->inject('swooleRequest') ->inject('request') ->inject('response') - ->inject('route') ->inject('console') ->inject('project') ->inject('dbForConsole') @@ -443,27 +450,7 @@ Http::init() ->inject('queueForUsage') ->inject('queueForEvents') ->inject('queueForCertificates') - ->inject('authorization') - ->action(function ( - Request $request, - Response $response, - Route $route, - Document $console, - Document $project, - Database $dbForConsole, - $getProjectDB, - Locale $locale, - array $localeCodes, - array $clients, - /** - * @disregard P1009 Undefined type - */ - Reader $geodb, - Usage $queueForUsage, - Event $queueForEvents, - Certificate $queueForCertificates, - Authorization $authorization - ) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates) { /* * Appwrite Router */ @@ -471,7 +458,7 @@ Http::init() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } @@ -479,8 +466,8 @@ Http::init() /* * Request format */ - //$route = $utopia->getRoute(); - //Request::setRoute($route); + $route = $utopia->getRoute(); + Request::setRoute($route); if ($route === null) { return $response @@ -512,7 +499,7 @@ Http::init() } elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) { Console::warning('Skipping SSL certificates generation on ACME challenge.'); } else { - $authorization->disable(); + Authorization::disable(); $envDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = null; @@ -520,7 +507,7 @@ Http::init() $mainDomain = $envDomain; } else { $domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]); - $mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get(); + $mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get(); } if ($mainDomain !== $domain->get()) { @@ -530,7 +517,7 @@ Http::init() Query::equal('domain', [$domain->get()]) ]); - if ($domainDocument->isEmpty()) { + if (!$domainDocument) { $domainDocument = new Document([ 'domain' => $domain->get(), 'resourceType' => 'api', @@ -551,12 +538,12 @@ Http::init() } $domains[$domain->get()] = true; - $authorization->reset(); // ensure authorization is re-enabled + Authorization::reset(); // ensure authorization is re-enabled } Config::setParam('domains', $domains); } - $localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); + $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); if (\in_array($localeParam, $localeCodes)) { $locale->setDefault($localeParam); } @@ -584,7 +571,7 @@ Http::init() Config::setParam( 'domainVerification', ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && - $endDomain->getRegisterable() !== '' + $endDomain->getRegisterable() !== '' ); $isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort(); @@ -599,8 +586,8 @@ Http::init() ? null : ( $isConsoleProject && $isConsoleRootSession - ? '.' . $selfDomain->getRegisterable() - : '.' . $request->getHostname() + ? '.' . $selfDomain->getRegisterable() + : '.' . $request->getHostname() ) ); @@ -619,7 +606,7 @@ Http::init() $response->addFilter(new ResponseV18()); } if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) { - $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); + $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); } } @@ -630,9 +617,7 @@ Http::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations - && ($request->getHeader('host') ?? '') !== 'localhost' - && ($request->getHeader('host') ?? '') !== APP_HOSTNAME_INTERNAL) { + if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } @@ -672,8 +657,9 @@ Http::init() } }); -Http::options() - ->inject('route') +App::options() + ->inject('utopia') + ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -681,8 +667,7 @@ Http::options() ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->inject('authorization') - ->action(function (Route $route, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Authorization $authorization) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -690,7 +675,7 @@ Http::options() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } @@ -707,25 +692,18 @@ Http::options() ->noContent(); }); -Http::error() +App::error() ->inject('error') - ->inject('user') - ->inject('route') + ->inject('utopia') ->inject('request') ->inject('response') ->inject('project') ->inject('logger') ->inject('log') - ->inject('authorization') - ->inject('connections') ->inject('queueForUsage') - ->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections, Usage $queueForUsage) { + ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - - if (is_null($route)) { - $route = new Route($request->getMethod(), $request->getURI()); - } - + $route = $utopia->getRoute(); $class = \get_class($error); $code = $error->getCode(); $message = $error->getMessage(); @@ -746,9 +724,9 @@ Http::error() Console::error('[Error] File: ' . $file); Console::error('[Error] Line: ' . $line); } + switch ($class) { - case 'Utopia\Servers\Exception': - case 'Utopia\Http\Exception': + case 'Utopia\Exception': $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); switch ($code) { case 400: @@ -793,36 +771,35 @@ Http::error() } else { $publish = $error->getCode() === 0 || $error->getCode() >= 500; } + if ($error->getCode() >= 400 && $error->getCode() < 500) { // Register error logger $providerName = System::getEnv('_APP_EXPERIMENT_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_EXPERIMENT_LOGGING_CONFIG', ''); - if (!(empty($providerName) || empty($providerConfig))) { - try { - $loggingProvider = new DSN($providerConfig); - $providerName = $loggingProvider->getScheme(); + try { + $loggingProvider = new DSN($providerConfig ?? ''); + $providerName = $loggingProvider->getScheme(); - if (!empty($providerName) && $providerName === 'sentry') { - $key = $loggingProvider->getPassword(); - $projectId = $loggingProvider->getUser() ?? ''; - $host = 'https://' . $loggingProvider->getHost(); + if (!empty($providerName) && $providerName === 'sentry') { + $key = $loggingProvider->getPassword(); + $projectId = $loggingProvider->getUser() ?? ''; + $host = 'https://' . $loggingProvider->getHost(); - $adapter = new Sentry($projectId, $key, $host); - $logger = new Logger($adapter); - $logger->setSample(0.04); - $publish = true; - } else { - throw new \Exception('Invalid experimental logging provider'); - } - } catch (\Throwable $th) { - Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); + $adapter = new Sentry($projectId, $key, $host); + $logger = new Logger($adapter); + $logger->setSample(0.04); + $publish = true; + } else { + throw new \Exception('Invalid experimental logging provider'); } + } catch (\Throwable $th) { + Console::warning('Failed to initialize logging provider: ' . $th->getMessage()); } } if ($publish && $project->getId() !== 'console') { - if (!Auth::isPrivilegedUser($authorization->getRoles())) { + if (!Auth::isPrivilegedUser(Authorization::getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -841,7 +818,14 @@ Http::error() } - if ($logger && ($publish || $error->getCode() === 0)) { + if ($logger && $publish) { + try { + /** @var Utopia\Database\Document $user */ + $user = $utopia->getResource('user'); + } catch (\Throwable) { + // All good, user is optional information for logger + } + if (isset($user) && !$user->isEmpty()) { $log->setUser(new User($user->getId())); } @@ -871,7 +855,7 @@ Http::error() $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', $authorization->getRoles()); + $log->addExtra('roles', Authorization::getRoles()); $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); $log->setAction($action); @@ -889,7 +873,7 @@ Http::error() /** Wrap all exceptions inside Appwrite\Extend\Exception */ if (!($error instanceof AppwriteException)) { - $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error); + $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); } switch ($code) { // Don't show 500 errors! @@ -909,14 +893,14 @@ Http::error() break; default: $code = 500; // All other errors get the generic 500 server error status code - $message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error'; + $message = 'Server Error'; } //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised $type = $error->getType(); - $output = ((Http::isDevelopment())) ? [ + $output = ((App::isDevelopment())) ? [ 'message' => $message, 'code' => $code, 'file' => $file, @@ -944,7 +928,7 @@ Http::error() $layout ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('development', Http::isDevelopment()) + ->setParam('development', App::isDevelopment()) ->setParam('projectName', $project->getAttribute('name')) ->setParam('projectURL', $project->getAttribute('url')) ->setParam('message', $output['message'] ?? '') @@ -955,18 +939,18 @@ Http::error() $response->html($layout->render()); } - $connections->reclaim(); - $response->dynamic( new Document($output), - Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR ); }); -Http::get('/robots.txt') +App::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') ->label('docs', false) + ->inject('utopia') + ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -974,9 +958,7 @@ Http::get('/robots.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->inject('route') - ->inject('authorization') - ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, ?Route $route, Authorization $authorization) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -984,17 +966,16 @@ Http::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (is_null($route)) { - $route = new Route($request->getMethod(), $request->getURI()); - } - router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); + router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); } }); -Http::get('/humans.txt') +App::get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) + ->inject('utopia') + ->inject('swooleRequest') ->inject('request') ->inject('response') ->inject('dbForConsole') @@ -1002,9 +983,7 @@ Http::get('/humans.txt') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->inject('route') - ->inject('authorization') - ->action(function (Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb, Route $route, Authorization $authorization) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1012,11 +991,11 @@ Http::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - router($dbForConsole, $getProjectDB, $request, $response, $route, $queueForEvents, $queueForUsage, $geodb, $authorization); + router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb); } }); -Http::get('/.well-known/acme-challenge/*') +App::get('/.well-known/acme-challenge/*') ->desc('SSL Verification') ->label('scope', 'public') ->label('docs', false) @@ -1066,32 +1045,16 @@ Http::get('/.well-known/acme-challenge/*') $response->text($content); }); -Http::wildcard() +include_once __DIR__ . '/shared/api.php'; +include_once __DIR__ . '/shared/api/auth.php'; + +App::wildcard() ->groups(['api']) ->label('scope', 'global') ->action(function () { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); -include_once 'mock.php'; -include_once 'shared/api.php'; -include_once 'shared/api/auth.php'; -include_once 'api/account.php'; -include_once 'api/avatars.php'; -include_once 'api/console.php'; -include_once 'api/databases.php'; -include_once 'api/functions.php'; -include_once 'api/graphql.php'; -include_once 'api/health.php'; -include_once 'api/locale.php'; -include_once 'api/messaging.php'; -include_once 'api/migrations.php'; -include_once 'api/project.php'; -include_once 'api/projects.php'; -include_once 'api/proxy.php'; -include_once 'api/storage.php'; -include_once 'api/teams.php'; -include_once 'api/users.php'; -include_once 'api/vcs.php'; -include_once 'web/console.php'; -include_once 'web/home.php'; +foreach (Config::getParam('services', []) as $service) { + include_once $service['controller']; +} diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 89e21cfb26..bc071fc885 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -3,7 +3,9 @@ global $utopia, $request, $response; use Appwrite\Extend\Exception; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -11,15 +13,13 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; -use Utopia\Http\Http; -use Utopia\Http\Route; -use Utopia\Http\Validator\Host; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\System\System; +use Utopia\Validator\Host; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; -Http::get('/v1/mock/tests/general/oauth2') +App::get('/v1/mock/tests/general/oauth2') ->desc('OAuth Login') ->groups(['mock']) ->label('scope', 'public') @@ -35,7 +35,7 @@ Http::get('/v1/mock/tests/general/oauth2') $response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state])); }); -Http::get('/v1/mock/tests/general/oauth2/token') +App::get('/v1/mock/tests/general/oauth2/token') ->desc('OAuth2 Token') ->groups(['mock']) ->label('scope', 'public') @@ -81,7 +81,7 @@ Http::get('/v1/mock/tests/general/oauth2/token') } }); -Http::get('/v1/mock/tests/general/oauth2/user') +App::get('/v1/mock/tests/general/oauth2/user') ->desc('OAuth2 User') ->groups(['mock']) ->label('scope', 'public') @@ -101,7 +101,7 @@ Http::get('/v1/mock/tests/general/oauth2/user') ]); }); -Http::get('/v1/mock/tests/general/oauth2/success') +App::get('/v1/mock/tests/general/oauth2/success') ->desc('OAuth2 Success') ->groups(['mock']) ->label('scope', 'public') @@ -114,7 +114,7 @@ Http::get('/v1/mock/tests/general/oauth2/success') ]); }); -Http::get('/v1/mock/tests/general/oauth2/failure') +App::get('/v1/mock/tests/general/oauth2/failure') ->desc('OAuth2 Failure') ->groups(['mock']) ->label('scope', 'public') @@ -129,7 +129,7 @@ Http::get('/v1/mock/tests/general/oauth2/failure') ]); }); -Http::patch('/v1/mock/functions-v2') +App::patch('/v1/mock/functions-v2') ->desc('Update Function Version to V2 (outdated code syntax)') ->groups(['mock', 'api', 'functions']) ->label('scope', 'functions.write') @@ -155,7 +155,7 @@ Http::patch('/v1/mock/functions-v2') $response->noContent(); }); -Http::post('/v1/mock/api-key-unprefixed') +App::post('/v1/mock/api-key-unprefixed') ->desc('Create API Key (without standard prefix)') ->groups(['mock', 'api', 'projects']) ->label('scope', 'public') @@ -204,7 +204,7 @@ Http::post('/v1/mock/api-key-unprefixed') ->dynamic($key, Response::MODEL_KEY); }); -Http::get('/v1/mock/github/callback') +App::get('/v1/mock/github/callback') ->desc('Create installation document using GitHub installation id') ->groups(['mock', 'api', 'vcs']) ->label('scope', 'public') @@ -264,13 +264,15 @@ Http::get('/v1/mock/github/callback') ]); }); -Http::shutdown() +App::shutdown() ->groups(['mock']) - ->inject('route') + ->inject('utopia') ->inject('response') - ->action(function (Route $route, Response $response) { + ->inject('request') + ->action(function (App $utopia, Response $response, Request $request) { $result = []; + $route = $utopia->getRoute(); $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index ee9012d74c..f0d896c95a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -15,11 +15,11 @@ use Appwrite\Event\Usage; use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; -use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; +use Utopia\App; use Utopia\Cache\Adapter\Filesystem; use Utopia\Cache\Cache; use Utopia\Config\Config; @@ -28,11 +28,8 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; -use Utopia\Http\Http; -use Utopia\Http\Route; -use Utopia\Http\Validator\WhiteList; use Utopia\System\System; +use Utopia\Validator\WhiteList; $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { preg_match_all('/{(.*?)}/', $label, $matches); @@ -153,9 +150,9 @@ $databaseListener = function (string $event, Document $document, Document $proje } }; -Http::init() +App::init() ->groups(['api']) - ->inject('route') + ->inject('utopia') ->inject('request') ->inject('dbForConsole') ->inject('project') @@ -164,8 +161,9 @@ Http::init() ->inject('servers') ->inject('mode') ->inject('team') - ->inject('authorization') - ->action(function (Route $route, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, Authorization $authorization) { + ->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) { + $route = $utopia->getRoute(); + if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } @@ -223,8 +221,8 @@ Http::init() $role = Auth::USER_ROLE_APPS; $scopes = \array_merge($roles[$role]['scopes'], $tokenScopes); - $authorization->addRole(Auth::USER_ROLE_APPS); - $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. + Authorization::setRole(Auth::USER_ROLE_APPS); + Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } } elseif ($keyType === API_KEY_STANDARD) { // No underline means no prefix. Backwards compatibility. @@ -249,8 +247,8 @@ Http::init() throw new Exception(Exception::PROJECT_KEY_EXPIRED); } - $authorization->addRole(Auth::USER_ROLE_APPS); - $authorization->setDefaultStatus(false); // Cancel security segmentation for API keys. + Authorization::setRole(Auth::USER_ROLE_APPS); + Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. $accessedAt = $key->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { @@ -296,15 +294,15 @@ Http::init() foreach ($adminRoles as $role) { $scopes = \array_merge($scopes, $roles[$role]['scopes']); } - $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. + + Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. } $scopes = \array_unique($scopes); - $authorization->addRole($role); - - foreach (Auth::getRoles($user, $authorization) as $authRole) { - $authorization->addRole($authRole); + Authorization::setRole($role); + foreach (Auth::getRoles($user) as $authRole) { + Authorization::setRole($authRole); } /** Do not allow access to disabled services */ @@ -313,7 +311,7 @@ Http::init() if ( array_key_exists($service, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -348,9 +346,9 @@ Http::init() } }); -Http::init() +App::init() ->groups(['api']) - ->inject('route') + ->inject('utopia') ->inject('request') ->inject('response') ->inject('project') @@ -364,12 +362,14 @@ Http::init() ->inject('queueForUsage') ->inject('dbForProject') ->inject('mode') - ->inject('authorization') - ->action(function (Route $route, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode, Authorization $authorization) use ($databaseListener) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) { + + $route = $utopia->getRoute(); + if ( array_key_exists('rest', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['rest'] - && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -399,7 +399,7 @@ Http::init() $closestLimit = null; - $roles = $authorization->getRoles(); + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -463,7 +463,7 @@ Http::init() $useCache = $route->getLabel('cache', false); if ($useCache) { $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); - $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) ); @@ -476,18 +476,19 @@ Http::init() if ($type === 'bucket') { $bucketId = $parts[1] ?? null; + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - - $isAPIKey = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead())); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -498,7 +499,7 @@ Http::init() if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -522,7 +523,7 @@ Http::init() } }); -Http::init() +App::init() ->groups(['session']) ->inject('user') ->inject('request') @@ -542,12 +543,14 @@ Http::init() * Delete older sessions if the number of sessions have crossed * the session limit set for the project */ -Http::shutdown() +App::shutdown() ->groups(['session']) + ->inject('utopia') + ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (Response $response, Document $project, Database $dbForProject) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, Database $dbForProject) { $sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT; $session = $response->getPayload(); $userId = $session['userId'] ?? ''; @@ -574,9 +577,9 @@ Http::shutdown() $dbForProject->purgeCachedDocument('users', $userId); }); -Http::shutdown() +App::shutdown() ->groups(['api']) - ->inject('route') + ->inject('utopia') ->inject('request') ->inject('response') ->inject('project') @@ -592,29 +595,7 @@ Http::shutdown() ->inject('queueForFunctions') ->inject('mode') ->inject('dbForConsole') - ->inject('authorization') - ->action(function ( - Route $route, - Request $request, - Response $response, - Document $project, - Document $user, - Event $queueForEvents, - Audit $queueForAudits, - Usage $queueForUsage, - Delete $queueForDeletes, - EventDatabase $queueForDatabase, - Build $queueForBuilds, - Messaging $queueForMessaging, - Database $dbForProject, - Func $queueForFunctions, - string $mode, - Database $dbForConsole, - Authorization $authorization, - ) use ($parseLabel) { - if (!empty($user) && !$user->isEmpty() && empty($user->getInternalId())) { - $user = $authorization->skip(fn () => $dbForProject->getDocument('users', $user->getId())); - } + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -674,6 +655,7 @@ Http::shutdown() } } + $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); /** @@ -743,11 +725,11 @@ Http::shutdown() $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); $signature = md5($data['payload']); - $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); + $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', ''); $now = DateTime::now(); if ($cacheLog->isEmpty()) { - $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([ + Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ '$id' => $key, 'resource' => $resource, 'resourceType' => $resourceType, @@ -757,7 +739,7 @@ Http::shutdown() ]))); } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); - $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); } if ($signature !== $cacheLog->getAttribute('signature')) { @@ -769,8 +751,10 @@ Http::shutdown() } } + + if ($project->getId() !== 'console') { - if (!Auth::isPrivilegedUser($authorization->getRoles())) { + if (!Auth::isPrivilegedUser(Authorization::getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -795,7 +779,7 @@ Http::shutdown() $accessedAt = $project->getAttribute('accessedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { $project->setAttribute('accessedAt', DateTime::now()); - $authorization->skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); + Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project)); } } @@ -816,16 +800,10 @@ Http::shutdown() } }); -Http::init() +App::init() ->groups(['usage']) ->action(function () { if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') { throw new Exception(Exception::GENERAL_USAGE_DISABLED); } }); - -Http::shutdown() - ->inject('connections') - ->action(function (Connections $connections) { - $connections->reclaim(); - }); diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index 224dcb3392..53aacabe21 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -4,14 +4,13 @@ use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Utopia\Request; use MaxMind\Db\Reader; +use Utopia\App; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\Http\Http; -use Utopia\Http\Route; use Utopia\System\System; -Http::init() +App::init() ->groups(['mfaProtected']) ->inject('session') ->action(function (Document $session) { @@ -30,14 +29,13 @@ Http::init() } }); -Http::init() +App::init() ->groups(['auth']) - ->inject('route') + ->inject('utopia') ->inject('request') ->inject('project') ->inject('geodb') - ->inject('authorization') - ->action(function (Route $route, Request $request, Document $project, Reader $geodb, Authorization $authorization) { + ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -48,17 +46,15 @@ Http::init() } } - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); + $route = $utopia->match($request); + + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAppUser = Auth::isAppUser(Authorization::getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; } - if ($route->getLabel('sdk.namespace', '') === 'graphql') { // Skip for graphQL recursive call - return; - } - $auths = $project->getAttribute('auths', []); switch ($route->getLabel('auth.type', '')) { case 'emailPassword': diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 60be4acf54..c02e140270 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -2,9 +2,9 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\Http\Http; +use Utopia\App; -Http::init() +App::init() ->groups(['web']) ->inject('request') ->inject('response') @@ -16,7 +16,7 @@ Http::init() ; }); -Http::get('/') +App::get('/') ->alias('auth/*') ->alias('/invite') ->alias('/login') diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 3577061d69..27b2614c37 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,10 +1,10 @@ desc('Get Version') ->groups(['home', 'web']) ->label('scope', 'public') diff --git a/app/http.php b/app/http.php index 320621ad33..bec772c770 100644 --- a/app/http.php +++ b/app/http.php @@ -1,242 +1,335 @@ true, - 'http_compression' => true, - 'http_compression_level' => 6, - 'package_max_length' => $payloadSize, - 'buffer_output_size' => $payloadSize, - // Server - // 'log_level' => 0, - 'dispatch_mode' => 2, - 'worker_num' => $workerNumber, - 'reactor_num' => swoole_cpu_num() * 2, - 'open_cpu_affinity' => true, - // Coroutine - 'enable_coroutine' => true, - 'send_yield' => true, - 'tcp_fastopen' => true, -]); +$http + ->set([ + 'worker_num' => $workerNumber, + 'open_http2_protocol' => true, + 'http_compression' => true, + 'http_compression_level' => 6, + 'package_max_length' => $payloadSize, + 'buffer_output_size' => $payloadSize, + ]); -$http = new Http($server, $container, 'UTC'); -$http->setRequestClass(Request::class); -$http->setResponseClass(Response::class); +$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) { + Console::success('Worker ' . ++$workerId . ' started successfully'); +}); -Http::onStart() - ->inject('authorization') - ->inject('cache') - ->inject('pools') - ->inject('connections') - ->action(function (Authorization $authorization, Cache $cache, array $pools, Connections $connections) { - try { - // wait for database to be ready - $attempts = 0; - $max = 15; - $sleep = 2; +$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server, $workerId) { + Console::success('Starting reload...'); +}); - do { - try { - $attempts++; - $pool = $pools['pools-console-console']['pool']; - $dsn = $pools['pools-console-console']['dsn']; - $connection = $pool->get(); - $connections->add($connection, $pool); +$http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) { + Console::success('Reload completed...'); +}); - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; +include __DIR__ . '/controllers/general.php'; - $adapter->setDatabase($dsn->getPath()); +$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) { + $app = new App('UTC'); - $dbForConsole = new Database($adapter, $cache); - $dbForConsole->setAuthorization($authorization); + go(function () use ($register, $app) { + $pools = $register->get('pools'); + /** @var Group $pools */ + App::setResource('pools', fn () => $pools); - $dbForConsole - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + // wait for database to be ready + $attempts = 0; + $max = 10; + $sleep = 1; - $dbForConsole->ping(); - break; // leave the do-while if successful - } catch (\Throwable $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: ' . $e->getMessage()); - } - sleep($sleep); + do { + try { + $attempts++; + $dbForConsole = $app->getResource('dbForConsole'); + /** @var Utopia\Database\Database $dbForConsole */ + break; // leave the do-while if successful + } catch (\Throwable $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: ' . $e->getMessage()); } - } while ($attempts < $max); + sleep($sleep); + } + } while ($attempts < $max); - Console::success('[Setup] - Server database init started...'); + Console::success('[Setup] - Server database init started...'); + + try { + Console::success('[Setup] - Creating database: appwrite...'); + $dbForConsole->create(); + } catch (\Throwable $e) { + Console::success('[Setup] - Skip: metadata table already exists'); + } + + if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { + $audit = new Audit($dbForConsole); + $audit->setup(); + } + + if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { + $adapter = new TimeLimit("", 0, 1, $dbForConsole); + $adapter->setup(); + } + + /** @var array $collections */ + $collections = Config::getParam('collections', []); + $consoleCollections = $collections['console']; + foreach ($consoleCollections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + if (!$dbForConsole->getCollection($key)->isEmpty()) { + continue; + } + + Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); + + $attributes = []; + $indexes = []; + + foreach ($collection['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($collection['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection($key, $attributes, $indexes); + } + + if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { + Console::success('[Setup] - Creating default bucket...'); + $dbForConsole->createDocument('buckets', new Document([ + '$id' => ID::custom('default'), + '$collection' => ID::custom('buckets'), + 'name' => 'Default', + 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'allowedFileExtensions' => [], + 'enabled' => true, + 'compression' => 'gzip', + 'encryption' => true, + 'antivirus' => true, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'search' => 'buckets Default', + ])); + + $bucket = $dbForConsole->getDocument('buckets', 'default'); + + Console::success('[Setup] - Creating files collection for default bucket...'); + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = []; + $indexes = []; + + foreach ($files['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($files['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } + + $pools->reclaim(); + + Console::success('[Setup] - Server database init completed...'); + }); + + Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)'); + Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}"); + + // listen ctrl + c + Process::signal(2, function () use ($http) { + Console::log('Stop by Ctrl+C'); + $http->shutdown(); + }); +}); + +$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) { + App::setResource('swooleRequest', fn () => $swooleRequest); + App::setResource('swooleResponse', fn () => $swooleResponse); + + $request = new Request($swooleRequest); + $response = new Response($swooleResponse); + + if (Files::isFileLoaded($request->getURI())) { + $time = (60 * 60 * 24 * 365 * 2); // 45 days cache + + $response + ->setContentType(Files::getFileMimeType($request->getURI())) + ->addHeader('Cache-Control', 'public, max-age=' . $time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache + ->send(Files::getFileContents($request->getURI())); + + return; + } + + $app = new App('UTC'); + + $pools = $register->get('pools'); + App::setResource('pools', fn () => $pools); + + try { + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + + $app->run($request, $response); + } catch (\Throwable $th) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + $logger = $app->getResource("logger"); + if ($logger) { + try { + /** @var Utopia\Database\Document $user */ + $user = $app->getResource('user'); + } catch (\Throwable $_th) { + // All good, user is optional information for logger + } + + $route = $app->getRoute(); + + $log = $app->getResource("log"); + + if (isset($user) && !$user->isEmpty()) { + $log->setUser(new User($user->getId())); + } + + $log->setNamespace("http"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($th->getMessage()); + + $log->addTag('method', $route->getMethod()); + $log->addTag('url', $route->getPath()); + $log->addTag('verboseType', get_class($th)); + $log->addTag('code', $th->getCode()); + // $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant + $log->addTag('hostname', $request->getHostname()); + $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); + + $log->addExtra('file', $th->getFile()); + $log->addExtra('line', $th->getLine()); + $log->addExtra('trace', $th->getTraceAsString()); + $log->addExtra('roles', Authorization::getRoles()); + + $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); + $log->setAction($action); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); try { - Console::success('[Setup] - Creating database: appwrite...'); - $dbForConsole->create(); - } catch (\Throwable $e) { - Console::success('[Setup] - Skip: metadata table already exists'); - return true; + $responseCode = $logger->addLog($log); + Console::info('Error log pushed with status code: ' . $responseCode); + } catch (Throwable $th) { + Console::error('Error pushing log: ' . $th->getMessage()); } - - if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { - $audit = new Audit($dbForConsole); - $audit->setup(); - } - - if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { - $abuse = new TimeLimit("", 0, 1, $dbForConsole); - $abuse->setup(); - } - - /** @var array $collections */ - $collections = Config::getParam('collections', []); - $consoleCollections = $collections['console']; - foreach ($consoleCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForConsole->getCollection($key)->isEmpty()) { - continue; - } - - Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); - - $attributes = []; - $indexes = []; - - foreach ($collection['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($collection['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection($key, $attributes, $indexes); - } - - if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { - Console::success('[Setup] - Creating default bucket...'); - $dbForConsole->createDocument('buckets', new Document([ - '$id' => ID::custom('default'), - '$collection' => ID::custom('buckets'), - 'name' => 'Default', - 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB - 'allowedFileExtensions' => [], - 'enabled' => true, - 'compression' => 'gzip', - 'encryption' => true, - 'antivirus' => true, - 'fileSecurity' => true, - '$permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'search' => 'buckets Default', - ])); - - $bucket = $dbForConsole->getDocument('buckets', 'default'); - - Console::success('[Setup] - Creating files collection for default bucket...'); - - $files = $collections['buckets']['files'] ?? []; - if (empty($files)) { - throw new Exception('Files collection is not configured.'); - } - - $attributes = []; - $indexes = []; - - foreach ($files['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($files['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); - } - - $connections->reclaim(); - - Console::success('[Setup] - Server database init completed...'); - Console::success('Server started successfully'); - } catch (\Throwable $e) { - Console::warning('Database not ready: ' . $e->getMessage()); - exit(1); } - }); -Http::init() - ->inject('authorization') - ->action(function (Authorization $authorization) { - $authorization->cleanRoles(); - $authorization->addRole(Role::any()->toString()); - }); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + + $swooleResponse->setStatusCode(500); + + $output = ((App::isDevelopment())) ? [ + 'message' => 'Error: ' . $th->getMessage(), + 'code' => 500, + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'trace' => $th->getTrace(), + 'version' => $version, + ] : [ + 'message' => 'Error: Server Error', + 'code' => 500, + 'version' => $version, + ]; + + $swooleResponse->end(\json_encode($output)); + } finally { + $pools->reclaim(); + } +}); $http->start(); diff --git a/app/init.php b/app/init.php index 0f9e8f0ef6..c2777f36f1 100644 --- a/app/init.php +++ b/app/init.php @@ -1,12 +1,26 @@ $value], JSON_PRESERVE_ZERO_FRACTION); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } -$registry->set('logger', function () { + return json_decode($value, true)['value']; + } +); + +Database::addFilter( + 'enum', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('elements')) { + $attribute->removeAttribute('elements'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['elements'])) { + $attribute->setAttribute('elements', $formatOptions['elements']); + } + + return $value; + } +); + +Database::addFilter( + 'range', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('min')) { + $attribute->removeAttribute('min'); + } + if ($attribute->isSet('max')) { + $attribute->removeAttribute('max'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['min']) || isset($formatOptions['max'])) { + $attribute + ->setAttribute('min', $formatOptions['min']) + ->setAttribute('max', $formatOptions['max']); + } + + return $value; + } +); + +Database::addFilter( + 'subQueryAttributes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $attributes = $database->find('attributes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForAttributes()), + ]); + + foreach ($attributes as $attribute) { + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options'); + foreach ($options as $key => $value) { + $attribute->setAttribute($key, $value); + } + $attribute->removeAttribute('options'); + } + } + + return $attributes; + } +); + +Database::addFilter( + 'subQueryIndexes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('indexes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForIndexes()), + ]); + } +); + +Database::addFilter( + 'subQueryPlatforms', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('platforms', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryKeys', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('keys', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQueryWebhooks', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('webhooks', [ + Query::equal('projectInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'subQuerySessions', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database->find('sessions', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryTokens', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database + ->find('tokens', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryChallenges', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database + ->find('challenges', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryAuthenticators', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database + ->find('authenticators', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryMemberships', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database + ->find('memberships', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); + +Database::addFilter( + 'subQueryVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceInternalId', [$document->getInternalId()]), + Query::equal('resourceType', ['function']), + Query::limit(APP_LIMIT_SUBQUERY), + ]); + } +); + +Database::addFilter( + 'encrypt', + function (mixed $value) { + $key = System::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + + return json_encode([ + 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag ?? ''), + 'version' => '1', + ]); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + $value = json_decode($value, true); + $key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']); + + return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); + } +); + +Database::addFilter( + 'subQueryProjectVariables', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('variables', [ + Query::equal('resourceType', ['project']), + Query::limit(APP_LIMIT_SUBQUERY) + ]); + } +); + +Database::addFilter( + 'userSearch', + function (mixed $value, Document $user) { + $searchValues = [ + $user->getId(), + $user->getAttribute('email', ''), + $user->getAttribute('name', ''), + $user->getAttribute('phone', '') + ]; + + foreach ($user->getAttribute('labels', []) as $label) { + $searchValues[] = 'label:' . $label; + } + + $search = implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'subQueryTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn () => $database + ->find('targets', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY) + ])); + } +); + +Database::addFilter( + 'subQueryTopicTargets', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $targetIds = Authorization::skip(fn () => \array_map( + fn ($document) => $document->getAttribute('targetInternalId'), + $database->find('subscribers', [ + Query::equal('topicInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) + ]) + )); + if (\count($targetIds) > 0) { + return $database->skipValidation(fn () => $database->find('targets', [ + Query::equal('$internalId', $targetIds) + ])); + } + return []; + } +); + +Database::addFilter( + 'providerSearch', + function (mixed $value, Document $provider) { + $searchValues = [ + $provider->getId(), + $provider->getAttribute('name', ''), + $provider->getAttribute('provider', ''), + $provider->getAttribute('type', '') + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'topicSearch', + function (mixed $value, Document $topic) { + $searchValues = [ + $topic->getId(), + $topic->getAttribute('name', ''), + $topic->getAttribute('description', ''), + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'messageSearch', + function (mixed $value, Document $message) { + $searchValues = [ + $message->getId(), + $message->getAttribute('description', ''), + $message->getAttribute('status', ''), + ]; + + $data = \json_decode($message->getAttribute('data', []), true); + $providerType = $message->getAttribute('providerType', ''); + + if ($providerType === MESSAGE_TYPE_EMAIL) { + $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); + } elseif ($providerType === MESSAGE_TYPE_SMS) { + $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); + } else { + $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); + } + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +/** + * DB Formats + */ +Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () { + return new Email(); +}, Database::VAR_STRING); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () { + return new DatetimeValidator(); +}, Database::VAR_DATETIME); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) { + $elements = $attribute['formatOptions']['elements']; + return new WhiteList($elements, true); +}, Database::VAR_STRING); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function () { + return new IP(); +}, Database::VAR_STRING); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function () { + return new URL(); +}, Database::VAR_STRING); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { + $min = $attribute['formatOptions']['min'] ?? -INF; + $max = $attribute['formatOptions']['max'] ?? INF; + return new Range($min, $max, Range::TYPE_INTEGER); +}, Database::VAR_INTEGER); + +Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { + $min = $attribute['formatOptions']['min'] ?? -INF; + $max = $attribute['formatOptions']['max'] ?? INF; + return new Range($min, $max, Range::TYPE_FLOAT); +}, Database::VAR_FLOAT); + +/* + * Registry + */ +$register->set('logger', function () { // Register error logger $providerName = System::getEnv('_APP_LOGGING_PROVIDER', ''); $providerConfig = System::getEnv('_APP_LOGGING_CONFIG', ''); @@ -105,12 +777,12 @@ $registry->set('logger', function () { default => ['key' => $loggingProvider->getHost()], }; } catch (Throwable $th) { - Console::warning('Using deprecated logging configuration. Please update your configuration to use DSN format.' . $th->getMessage()); // Fallback for older Appwrite versions up to 1.5.x that use _APP_LOGGING_PROVIDER and _APP_LOGGING_CONFIG environment variables + Console::warning('Using deprecated logging configuration. Please update your configuration to use DSN format.' . $th->getMessage()); $configChunks = \explode(";", $providerConfig); $providerConfig = match ($providerName) { - 'sentry' => ['key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], + 'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',], 'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''], default => ['key' => $providerConfig], }; @@ -141,167 +813,200 @@ $registry->set('logger', function () { return; } - $logger = new Logger($adapter); - $logger->setSample(0.4); - return $logger; + return new Logger($adapter); }); -$registry->set('geodb', function () { - /** - * @disregard P1009 Undefined type - */ - return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); -}); +$register->set('pools', function () { + $group = new Group(); -$registry->set('hooks', function () { - return new Hooks(); -}); + $fallbackForDB = 'db_main=' . AppwriteURL::unparse([ + 'scheme' => 'mariadb', + 'host' => System::getEnv('_APP_DB_HOST', 'mariadb'), + 'port' => System::getEnv('_APP_DB_PORT', '3306'), + 'user' => System::getEnv('_APP_DB_USER', ''), + 'pass' => System::getEnv('_APP_DB_PASS', ''), + 'path' => System::getEnv('_APP_DB_SCHEMA', ''), + ]); + $fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([ + 'scheme' => 'redis', + 'host' => System::getEnv('_APP_REDIS_HOST', 'redis'), + 'port' => System::getEnv('_APP_REDIS_PORT', '6379'), + 'user' => System::getEnv('_APP_REDIS_USER', ''), + 'pass' => System::getEnv('_APP_REDIS_PASS', ''), + ]); -$registry->set( - 'pools', - (function () { - $fallbackForDB = 'db_main=' . URL::unparse([ - 'scheme' => 'mariadb', - 'host' => System::getEnv('_APP_DB_HOST', 'mariadb'), - 'port' => System::getEnv('_APP_DB_PORT', '3306'), - 'user' => System::getEnv('_APP_DB_USER', ''), - 'pass' => System::getEnv('_APP_DB_PASS', ''), - 'path' => System::getEnv('_APP_DB_SCHEMA', ''), - ]); - $fallbackForRedis = 'redis_main=' . URL::unparse([ - 'scheme' => 'redis', - 'host' => System::getEnv('_APP_REDIS_HOST', 'redis'), - 'port' => System::getEnv('_APP_REDIS_PORT', '6379'), - 'user' => System::getEnv('_APP_REDIS_USER', ''), - 'pass' => System::getEnv('_APP_REDIS_PASS', ''), - ]); + $connections = [ + 'console' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), + 'multiple' => false, + 'schemes' => ['mariadb', 'mysql'], + ], + 'database' => [ + 'type' => 'database', + 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), + 'multiple' => true, + 'schemes' => ['mariadb', 'mysql'], + ], + 'queue' => [ + 'type' => 'queue', + 'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'pubsub' => [ + 'type' => 'pubsub', + 'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), + 'multiple' => false, + 'schemes' => ['redis'], + ], + 'cache' => [ + 'type' => 'cache', + 'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), + 'multiple' => true, + 'schemes' => ['redis'], + ], + ]; - $connections = [ - 'console' => [ - 'type' => 'database', - 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB), - 'multiple' => false, - 'schemes' => ['mariadb', 'mysql'], - ], - 'database' => [ - 'type' => 'database', - 'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB), - 'multiple' => true, - 'schemes' => ['mariadb', 'mysql'], - ], - 'queue' => [ - 'type' => 'queue', - 'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'pubsub' => [ - 'type' => 'pubsub', - 'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis), - 'multiple' => false, - 'schemes' => ['redis'], - ], - 'cache' => [ - 'type' => 'cache', - 'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis), - 'multiple' => true, - 'schemes' => ['redis'], - ], - ]; + $maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151); + $instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14); - $pools = []; - $poolSize = (int)System::getEnv('_APP_POOL_SIZE', 64); + $multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled'; - foreach ($connections as $key => $connection) { - $dsns = $connection['dsns'] ?? ''; - $multiple = $connection['multiple'] ?? false; - $schemes = $connection['schemes'] ?? []; - $dsns = explode(',', $connection['dsns'] ?? ''); - $config = []; + if ($multiprocessing) { + $workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6)); + } else { + $workerCount = 1; + } - foreach ($dsns as &$dsn) { - $dsn = explode('=', $dsn); - $name = ($multiple) ? $key . '_' . $dsn[0] : $key; - $config[] = $name; - $dsn = $dsn[1] ?? ''; + if ($workerCount > $instanceConnections) { + throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500); + } - if (empty($dsn)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); - } + $poolSize = (int)($instanceConnections / $workerCount); - $dsn = new DSN($dsn); - $dsnHost = $dsn->getHost(); - $dsnPort = $dsn->getPort(); - $dsnUser = $dsn->getUser(); - $dsnPass = $dsn->getPassword(); - $dsnScheme = $dsn->getScheme(); - $dsnDatabase = $dsn->getPath(); + foreach ($connections as $key => $connection) { + $type = $connection['type'] ?? ''; + $multiple = $connection['multiple'] ?? false; + $schemes = $connection['schemes'] ?? []; + $config = []; + $dsns = explode(',', $connection['dsns'] ?? ''); + foreach ($dsns as &$dsn) { + $dsn = explode('=', $dsn); + $name = ($multiple) ? $key . '_' . $dsn[0] : $key; + $dsn = $dsn[1] ?? ''; + $config[] = $name; + if (empty($dsn)) { + //throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}"); + continue; + } - if (!in_array($dsnScheme, $schemes)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); - } + $dsn = new DSN($dsn); + $dsnHost = $dsn->getHost(); + $dsnPort = $dsn->getPort(); + $dsnUser = $dsn->getUser(); + $dsnPass = $dsn->getPassword(); + $dsnScheme = $dsn->getScheme(); + $dsnDatabase = $dsn->getPath(); - /** - * Get Resource - * - * Creation could be reused accross connection types like database, cache, queue, etc. - * - * Resource assignment to an adapter will happen below. - */ - switch ($dsnScheme) { - case 'mysql': - case 'mariadb': - $pool = new PDOPool( - (new PDOConfig()) - ->withHost($dsnHost) - ->withPort($dsnPort) - ->withDbName($dsnDatabase) - ->withCharset('utf8mb4') - ->withUsername($dsnUser) - ->withPassword($dsnPass) - ->withOptions([ - // No need to set PDO::ATTR_ERRMODE it is overwitten in PDOProxy - // PDO::ATTR_TIMEOUT => 3, // Seconds - // PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => true, - PDO::ATTR_STRINGIFY_FETCHES => true, - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + if (!in_array($dsnScheme, $schemes)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme"); + } - ]), - $poolSize - ); + /** + * Get Resource + * + * Creation could be reused across connection types like database, cache, queue, etc. + * + * Resource assignment to an adapter will happen below. + */ + $resource = match ($dsnScheme) { + 'mysql', + 'mariadb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { + return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { + return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true + )); + }); + }, + 'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) { + $redis = new Redis(); + @$redis->pconnect($dsnHost, (int)$dsnPort); + if ($dsnPass) { + $redis->auth($dsnPass); + } + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; + }, + default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'), + }; + + $pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) { + // Get Adapter + switch ($type) { + case 'database': + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($resource()), + 'mysql' => new MySQL($resource()), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); break; - case 'redis': - $pool = new RedisPool( - (new RedisConfig()) - ->withHost($dsnHost) - ->withPort((int)$dsnPort) - ->withAuth($dsnPass ?? ''), - $poolSize - ); + case 'pubsub': + $adapter = $resource(); + break; + case 'queue': + $adapter = match ($dsn->getScheme()) { + 'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()), + default => null + }; + break; + case 'cache': + $adapter = match ($dsn->getScheme()) { + 'redis' => new RedisCache($resource()), + default => null + }; break; default: - throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"); + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); } - $pools['pools-' . $key . '-' . $name] = [ - 'pool' => $pool, - 'dsn' => $dsn, - ]; - } + return $adapter; + }); - Config::setParam('pools-' . $key, $config); + $group->add($pool); } - return function () use ($pools): array { - return $pools; - }; - })() -); + Config::setParam('pools-' . $key, $config); + } -$registry->set('smtp', function () { + return $group; +}); + +$register->set('db', function () { + // This is usually for our workers or CLI commands scope + $dbHost = System::getEnv('_APP_DB_HOST', ''); + $dbPort = System::getEnv('_APP_DB_PORT', ''); + $dbUser = System::getEnv('_APP_DB_USER', ''); + $dbPass = System::getEnv('_APP_DB_PASS', ''); + $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); + + return new PDO( + "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", + $dbUser, + $dbPass, + SQL::getPDOAttributes() + ); +}); + +$register->set('smtp', function () { $mail = new PHPMailer(true); $mail->isSMTP(); @@ -329,65 +1034,771 @@ $registry->set('smtp', function () { return $mail; }); - -$registry->set('promiseAdapter', function () { +$register->set('geodb', function () { + return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-09.mmdb'); +}); +$register->set('passwordsDictionary', function () { + $content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords'); + $content = explode("\n", $content); + $content = array_flip($content); + return $content; +}); +$register->set('promiseAdapter', function () { return new Swoole(); }); +$register->set('hooks', function () { + return new Hooks(); +}); +/* + * Localization + */ +Locale::$exceptions = false; -$registry->set('db', function () { - // This is usually for our workers or CLI commands scope - $dbHost = System::getEnv('_APP_DB_HOST', ''); - $dbPort = System::getEnv('_APP_DB_PORT', ''); - $dbUser = System::getEnv('_APP_DB_USER', ''); - $dbPass = System::getEnv('_APP_DB_PASS', ''); - $dbScheme = System::getEnv('_APP_DB_SCHEMA', ''); +$locales = Config::getParam('locale-codes', []); - return new PDO( - "mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", - $dbUser, - $dbPass, - SQL::getPDOAttributes() - ); +foreach ($locales as $locale) { + $code = $locale['code']; + + $path = __DIR__ . '/config/locale/translations/' . $code . '.json'; + + if (!\file_exists($path)) { + $path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar` + if (!\file_exists($path)) { + $path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json` + } + } + + Locale::setLanguageFromJSON($code, $path); +} + +\stream_context_set_default([ // Set global user agent and http settings + 'http' => [ + 'method' => 'GET', + 'user_agent' => \sprintf( + APP_USERAGENT, + System::getEnv('_APP_VERSION', 'UNKNOWN'), + System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) + ), + 'timeout' => 2, + ], +]); + +// Runtime Execution +App::setResource('log', fn () => new Log()); +App::setResource('logger', function ($register) { + return $register->get('logger'); +}, ['register']); + +App::setResource('hooks', function ($register) { + return $register->get('hooks'); +}, ['register']); + +App::setResource('register', fn () => $register); +App::setResource('locale', fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); + +App::setResource('localeCodes', function () { + return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])); }); -// Autoload -class_exists(JWT::class, true); -class_exists(DSN::class, true); -class_exists(Log::class, true); -class_exists(TOTP::class, true); -class_exists(Mail::class, true); -class_exists(Func::class, true); -class_exists(Cache::class, true); -class_exists(Abuse::class, true); -class_exists(MySQL::class, true); -class_exists(Event::class, true); -class_exists(Audit::class, true); -class_exists(Usage::class, true); -class_exists(Local::class, true); -class_exists(Build::class, true); -class_exists(Locale::class, true); -class_exists(Delete::class, true); -class_exists(GitHub::class, true); -class_exists(Schema::class, true); -class_exists(Domain::class, true); -class_exists(Console::class, true); -class_exists(Request::class, true); -class_exists(MariaDB::class, true); -class_exists(Document::class, true); -class_exists(Sharding::class, true); -class_exists(Database::class, true); -class_exists(Hostname::class, true); -class_exists(TimeLimit::class, true); -class_exists(Migration::class, true); -class_exists(Messaging::class, true); -class_exists(CacheRedis::class, true); -class_exists(Connections::class, true); -class_exists(Certificate::class, true); -class_exists(EventDatabase::class, true); -class_exists(Authorization::class, true); -class_exists(Authentication::class, true); -class_exists(Queue\Connection\Redis::class, true); +// Queues +App::setResource('queue', function (Group $pools) { + return $pools->get('queue')->pop()->getResource(); +}, ['pools']); +App::setResource('queueForMessaging', function (Connection $queue) { + return new Messaging($queue); +}, ['queue']); +App::setResource('queueForMails', function (Connection $queue) { + return new Mail($queue); +}, ['queue']); +App::setResource('queueForBuilds', function (Connection $queue) { + return new Build($queue); +}, ['queue']); +App::setResource('queueForDatabase', function (Connection $queue) { + return new EventDatabase($queue); +}, ['queue']); +App::setResource('queueForDeletes', function (Connection $queue) { + return new Delete($queue); +}, ['queue']); +App::setResource('queueForEvents', function (Connection $queue) { + return new Event($queue); +}, ['queue']); +App::setResource('queueForAudits', function (Connection $queue) { + return new Audit($queue); +}, ['queue']); +App::setResource('queueForFunctions', function (Connection $queue) { + return new Func($queue); +}, ['queue']); +App::setResource('queueForUsage', function (Connection $queue) { + return new Usage($queue); +}, ['queue']); +App::setResource('queueForCertificates', function (Connection $queue) { + return new Certificate($queue); +}, ['queue']); +App::setResource('queueForMigrations', function (Connection $queue) { + return new Migration($queue); +}, ['queue']); +App::setResource('clients', function ($request, $console, $project) { + $console->setAttribute('platforms', [ // Always allow current host + '$collection' => ID::custom('platforms'), + 'name' => 'Current Host', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => $request->getHostname(), + ], Document::SET_TYPE_APPEND); -require_once __DIR__ . '/init/resources.php'; + $hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', '')); + $validator = new Hostname(); + foreach ($hostnames as $hostname) { + $hostname = trim($hostname); + if (!$validator->isValid($hostname)) { + continue; + } + $console->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Origin::CLIENT_TYPE_WEB, + 'name' => $hostname, + 'hostname' => $hostname, + ], Document::SET_TYPE_APPEND); + } -Models::init(); + /** + * Get All verified client URLs for both console and current projects + * + Filter for duplicated entries + */ + $clientsConsole = \array_map( + fn ($node) => $node['hostname'], + \array_filter( + $console->getAttribute('platforms', []), + fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && !empty($node['hostname'])) + ) + ); + + $clients = $clientsConsole; + $platforms = $project->getAttribute('platforms', []); + + foreach ($platforms as $node) { + if ( + isset($node['type']) && + ($node['type'] === Origin::CLIENT_TYPE_WEB || + $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && + !empty($node['hostname']) + ) { + $clients[] = $node['hostname']; + } + } + + return \array_unique($clients); +}, ['request', 'console', 'project']); + +App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) { + /** @var Appwrite\Utopia\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Document $project */ + /** @var Utopia\Database\Database $dbForProject */ + /** @var Utopia\Database\Database $dbForConsole */ + /** @var string $mode */ + + Authorization::setDefaultStatus(true); + + Auth::setCookieName('a_session_' . $project->getId()); + + if (APP_MODE_ADMIN === $mode) { + Auth::setCookieName('a_session_' . $console->getId()); + } + + $session = Auth::decodeSession( + $request->getCookie( + Auth::$cookieName, // Get sessions + $request->getCookie(Auth::$cookieName . '_legacy', '') + ) + ); + + // Get session from header for SSR clients + if (empty($session['id']) && empty($session['secret'])) { + $sessionHeader = $request->getHeader('x-appwrite-session', ''); + + if (!empty($sessionHeader)) { + $session = Auth::decodeSession($sessionHeader); + } + } + + // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies + if ($response) { + $response->addHeader('X-Debug-Fallback', 'false'); + } + + if (empty($session['id']) && empty($session['secret'])) { + if ($response) { + $response->addHeader('X-Debug-Fallback', 'true'); + } + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); + } + + Auth::$unique = $session['id'] ?? ''; + Auth::$secret = $session['secret'] ?? ''; + + if (APP_MODE_ADMIN !== $mode) { + if ($project->isEmpty()) { + $user = new Document([]); + } else { + if ($project->getId() === 'console') { + $user = $dbForConsole->getDocument('users', Auth::$unique); + } else { + $user = $dbForProject->getDocument('users', Auth::$unique); + } + } + } else { + $user = $dbForConsole->getDocument('users', Auth::$unique); + } + + if ( + $user->isEmpty() // Check a document has been found in the DB + || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) + ) { // Validate user has valid login token + $user = new Document([]); + } + + // if (APP_MODE_ADMIN === $mode) { + // if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { + // Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + // } else { + // $user = new Document([]); + // } + // } + + $authJWT = $request->getHeader('x-appwrite-jwt', ''); + + if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); + } + + $jwtUserId = $payload['userId'] ?? ''; + if (!empty($jwtUserId)) { + $user = $dbForProject->getDocument('users', $jwtUserId); + } + + $jwtSessionId = $payload['sessionId'] ?? ''; + if (!empty($jwtSessionId)) { + if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token + $user = new Document([]); + } + } + } + + $dbForProject->setMetadata('user', $user->getId()); + $dbForConsole->setMetadata('user', $user->getId()); + + return $user; +}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']); + +App::setResource('project', function ($dbForConsole, $request, $console) { + /** @var Appwrite\Utopia\Request $request */ + /** @var Utopia\Database\Database $dbForConsole */ + /** @var Utopia\Database\Document $console */ + + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + + if (empty($projectId) || $projectId === 'console') { + return $console; + } + + $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + + return $project; +}, ['dbForConsole', 'request', 'console']); + +App::setResource('session', function (Document $user) { + if ($user->isEmpty()) { + return; + } + + $sessions = $user->getAttribute('sessions', []); + $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); + + if (!$sessionId) { + return; + } + + foreach ($sessions as $session) {/** @var Document $session */ + if ($sessionId === $session->getId()) { + return $session; + } + } + + return; +}, ['user']); + +App::setResource('console', function () { + return new Document([ + '$id' => ID::custom('console'), + '$internalId' => ID::custom('console'), + 'name' => 'Appwrite', + '$collection' => ID::custom('projects'), + 'description' => 'Appwrite core engine', + 'logo' => '', + 'teamId' => null, + 'webhooks' => [], + 'keys' => [], + 'platforms' => [ + [ + '$collection' => ID::custom('platforms'), + 'name' => 'Localhost', + 'type' => Origin::CLIENT_TYPE_WEB, + 'hostname' => 'localhost', + ], // Current host is added on app init + ], + 'legalName' => '', + 'legalCountry' => '', + 'legalState' => '', + 'legalCity' => '', + 'legalAddress' => '', + 'legalTaxId' => '', + 'auths' => [ + 'mockNumbers' => [], + 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', + 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user + 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds + 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' + ], + 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], + 'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'oAuthProviders' => [ + 'githubEnabled' => true, + 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), + 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') + ], + ]); +}, []); + +App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $dbAdapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); + + $database = new Database($dbAdapter, $cache); + + $database + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; +}, ['pools', 'dbForConsole', 'cache', 'project']); + +App::setResource('dbForConsole', function (Group $pools, Cache $cache) { + $dbAdapter = $pools + ->get('console') + ->pop() + ->getResource(); + + $database = new Database($dbAdapter, $cache); + + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + return $database; +}, ['pools', 'cache']); + +App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $configure = (function (Database $database) use ($project, $dsn) { + $database + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + }); + + if (isset($databases[$dsn->getHost()])) { + $database = $databases[$dsn->getHost()]; + $configure($database); + return $database; + } + + $dbAdapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); + + $database = new Database($dbAdapter, $cache); + $databases[$dsn->getHost()] = $database; + $configure($database); + + return $database; + }; +}, ['pools', 'dbForConsole', 'cache']); + +App::setResource('cache', function (Group $pools) { + $list = Config::getParam('pools-cache', []); + $adapters = []; + + foreach ($list as $value) { + $adapters[] = $pools + ->get($value) + ->pop() + ->getResource() + ; + } + + return new Cache(new Sharding($adapters)); +}, ['pools']); + +App::setResource('deviceForLocal', function () { + return new Local(); +}); + +App::setResource('deviceForFiles', function ($project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); +}, ['project']); + +App::setResource('deviceForFunctions', function ($project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); +}, ['project']); + +App::setResource('deviceForBuilds', function ($project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); +}, ['project']); + +function getDevice(string $root, string $connection = ''): Device +{ + $connection = !empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', ''); + + if (!empty($connection)) { + $acl = 'private'; + $device = Storage::DEVICE_LOCAL; + $accessKey = ''; + $accessSecret = ''; + $bucket = ''; + $region = ''; + + try { + $dsn = new DSN($connection); + $device = $dsn->getScheme(); + $accessKey = $dsn->getUser() ?? ''; + $accessSecret = $dsn->getPassword() ?? ''; + $bucket = $dsn->getPath() ?? ''; + $region = $dsn->getParam('region'); + } catch (\Throwable $e) { + Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); + } + + switch ($device) { + case Storage::DEVICE_S3: + return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case STORAGE::DEVICE_DO_SPACES: + $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); + $device->setHttpVersion(S3::HTTP_VERSION_1_1); + return $device; + case Storage::DEVICE_BACKBLAZE: + return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LINODE: + return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_WASABI: + return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + } + } else { + switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { + case Storage::DEVICE_LOCAL: + default: + return new Local($root); + case Storage::DEVICE_S3: + $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); + $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); + $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); + $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); + $s3Acl = 'private'; + return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); + case Storage::DEVICE_DO_SPACES: + $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); + $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); + $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); + $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); + $doSpacesAcl = 'private'; + $device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); + $device->setHttpVersion(S3::HTTP_VERSION_1_1); + return $device; + case Storage::DEVICE_BACKBLAZE: + $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); + $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); + $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); + $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); + $backblazeAcl = 'private'; + return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); + case Storage::DEVICE_LINODE: + $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); + $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); + $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); + $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); + $linodeAcl = 'private'; + return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); + case Storage::DEVICE_WASABI: + $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); + $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); + $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); + $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); + $wasabiAcl = 'private'; + return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); + } + } +} + +App::setResource('mode', function ($request) { + /** @var Appwrite\Utopia\Request $request */ + + /** + * Defines the mode for the request: + * - 'default' => Requests for Client and Server Side + * - 'admin' => Request from the Console on non-console projects + */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); +}, ['request']); + +App::setResource('geodb', function ($register) { + /** @var Utopia\Registry\Registry $register */ + return $register->get('geodb'); +}, ['register']); + +App::setResource('passwordsDictionary', function ($register) { + /** @var Utopia\Registry\Registry $register */ + return $register->get('passwordsDictionary'); +}, ['register']); + + +App::setResource('servers', function () { + $platforms = Config::getParam('platforms'); + $server = $platforms[APP_PLATFORM_SERVER]; + + $languages = array_map(function ($language) { + return strtolower($language['name']); + }, $server['sdks']); + + return $languages; +}); + +App::setResource('promiseAdapter', function ($register) { + return $register->get('promiseAdapter'); +}, ['register']); + +App::setResource('schema', function ($utopia, $dbForProject) { + + $complexity = function (int $complexity, array $args) { + $queries = Query::parseQueries($args['queries'] ?? []); + $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; + $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; + + return $complexity * $limit; + }; + + $attributes = function (int $limit, int $offset) use ($dbForProject) { + $attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [ + Query::limit($limit), + Query::offset($offset), + ])); + + return \array_map(function ($attr) { + return $attr->getArrayCopy(); + }, $attrs); + }; + + $urls = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents"; + }, + 'read' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + 'delete' => function (string $databaseId, string $collectionId, array $args) { + return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; + }, + ]; + + $params = [ + 'list' => function (string $databaseId, string $collectionId, array $args) { + return [ 'queries' => $args['queries']]; + }, + 'create' => function (string $databaseId, string $collectionId, array $args) { + $id = $args['id'] ?? 'unique()'; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'documentId' => $id, + 'collectionId' => $collectionId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + 'update' => function (string $databaseId, string $collectionId, array $args) { + $documentId = $args['id']; + $permissions = $args['permissions'] ?? null; + + unset($args['id']); + unset($args['permissions']); + + // Order must be the same as the route params + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => $documentId, + 'data' => $args, + 'permissions' => $permissions, + ]; + }, + ]; + + return Schema::build( + $utopia, + $complexity, + $attributes, + $urls, + $params, + ); +}, ['utopia', 'dbForProject']); + +App::setResource('contributors', function () { + $path = 'app/config/contributors.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}); + +App::setResource('employees', function () { + $path = 'app/config/employees.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}); + +App::setResource('heroes', function () { + $path = 'app/config/heroes.json'; + $list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + return $list; +}); + +App::setResource('gitHub', function (Cache $cache) { + return new VcsGitHub($cache); +}, ['cache']); + +App::setResource('requestTimestamp', function ($request) { + //TODO: Move this to the Request class itself + $timestampHeader = $request->getHeader('x-appwrite-timestamp'); + $requestTimestamp = null; + if (!empty($timestampHeader)) { + try { + $requestTimestamp = new \DateTime($timestampHeader); + } catch (\Throwable $e) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); + } + } + return $requestTimestamp; +}, ['request']); +App::setResource('plan', function (array $plan = []) { + return []; +}); + + +App::setResource('team', function (Document $project, Database $dbForConsole, App $utopia, Request $request) { + $teamInternalId = ''; + if ($project->getId() !== 'console') { + $teamInternalId = $project->getAttribute('teamInternalId', ''); + } else { + $route = $utopia->match($request); + $path = $route->getPath(); + if (str_starts_with($path, '/v1/projects/:projectId')) { + $uri = $request->getURI(); + $pid = explode('/', $uri)[3]; + $p = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $pid)); + $teamInternalId = $p->getAttribute('teamInternalId', ''); + } elseif ($path === '/v1/projects') { + $teamId = $request->getParam('teamId', ''); + $team = Authorization::skip(fn () => $dbForConsole->getDocument('teams', $teamId)); + return $team; + } + } + + $team = Authorization::skip(function () use ($dbForConsole, $teamInternalId) { + return $dbForConsole->findOne('teams', [ + Query::equal('$internalId', [$teamInternalId]), + ]); + }); + + if (!$team) { + $team = new Document([]); + } + return $team; +}, ['project', 'dbForConsole', 'utopia', 'request']); diff --git a/app/init/config.php b/app/init/config.php deleted file mode 100644 index bc8333b5d9..0000000000 --- a/app/init/config.php +++ /dev/null @@ -1,37 +0,0 @@ - $value], JSON_PRESERVE_ZERO_FRACTION); - }, - function (mixed $value) { - if (is_null($value)) { - return; - } - - return json_decode($value, true)['value']; - } -); - -Database::addFilter( - 'enum', - function (mixed $value, Document $attribute) { - if ($attribute->isSet('elements')) { - $attribute->removeAttribute('elements'); - } - - return $value; - }, - function (mixed $value, Document $attribute) { - $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); - if (isset($formatOptions['elements'])) { - $attribute->setAttribute('elements', $formatOptions['elements']); - } - - return $value; - } -); - -Database::addFilter( - 'range', - function (mixed $value, Document $attribute) { - if ($attribute->isSet('min')) { - $attribute->removeAttribute('min'); - } - if ($attribute->isSet('max')) { - $attribute->removeAttribute('max'); - } - - return $value; - }, - function (mixed $value, Document $attribute) { - $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); - if (isset($formatOptions['min']) || isset($formatOptions['max'])) { - $attribute - ->setAttribute('min', $formatOptions['min']) - ->setAttribute('max', $formatOptions['max']) - ; - } - - return $value; - } -); - -Database::addFilter( - 'subQueryAttributes', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - $attributes = $database->find('attributes', [ - Query::equal('collectionInternalId', [$document->getInternalId()]), - Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), - Query::limit($database->getLimitForAttributes()), - ]); - - foreach ($attributes as $attribute) { - if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { - $options = $attribute->getAttribute('options'); - foreach ($options as $key => $value) { - $attribute->setAttribute($key, $value); - } - $attribute->removeAttribute('options'); - } - } - - return $attributes; - } -); - -Database::addFilter( - 'subQueryIndexes', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('indexes', [ - Query::equal('collectionInternalId', [$document->getInternalId()]), - Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), - Query::limit($database->getLimitForIndexes()), - ]); - } -); - -Database::addFilter( - 'subQueryPlatforms', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('platforms', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQueryKeys', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('keys', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQueryWebhooks', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('webhooks', [ - Query::equal('projectInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'subQuerySessions', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database->find('sessions', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryTokens', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database - ->find('tokens', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryChallenges', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database - ->find('challenges', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryAuthenticators', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database - ->find('authenticators', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryMemberships', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database - ->find('memberships', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY), - ])); - } -); - -Database::addFilter( - 'subQueryVariables', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('variables', [ - Query::equal('resourceInternalId', [$document->getInternalId()]), - Query::equal('resourceType', ['function']), - Query::limit(APP_LIMIT_SUBQUERY), - ]); - } -); - -Database::addFilter( - 'encrypt', - function (mixed $value) { - $key = System::getEnv('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - - return json_encode([ - 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag ?? ''), - 'version' => '1', - ]); - }, - function (mixed $value) { - if (is_null($value)) { - return; - } - $value = json_decode($value, true); - $key = System::getEnv('_APP_OPENSSL_KEY_V' . $value['version']); - - return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); - } -); - -Database::addFilter( - 'subQueryProjectVariables', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database - ->find('variables', [ - Query::equal('resourceType', ['project']), - Query::limit(APP_LIMIT_SUBQUERY) - ]); - } -); - -Database::addFilter( - 'userSearch', - function (mixed $value, Document $user) { - $searchValues = [ - $user->getId(), - $user->getAttribute('email', ''), - $user->getAttribute('name', ''), - $user->getAttribute('phone', '') - ]; - - foreach ($user->getAttribute('labels', []) as $label) { - $searchValues[] = 'label:' . $label; - } - - $search = implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'subQueryTargets', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - return $database->getAuthorization()->skip(fn () => $database - ->find('targets', [ - Query::equal('userInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBQUERY) - ])); - } -); - -Database::addFilter( - 'subQueryTopicTargets', - function (mixed $value) { - return; - }, - function (mixed $value, Document $document, Database $database) { - $targetIds = $database->getAuthorization()->skip(fn () => \array_map( - fn ($document) => $document->getAttribute('targetInternalId'), - $database->find('subscribers', [ - Query::equal('topicInternalId', [$document->getInternalId()]), - Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) - ]) - )); - if (\count($targetIds) > 0) { - return $database->find('targets', [ - Query::equal('$internalId', $targetIds) - ]); - } - return []; - } -); - -Database::addFilter( - 'providerSearch', - function (mixed $value, Document $provider) { - $searchValues = [ - $provider->getId(), - $provider->getAttribute('name', ''), - $provider->getAttribute('provider', ''), - $provider->getAttribute('type', '') - ]; - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'topicSearch', - function (mixed $value, Document $topic) { - $searchValues = [ - $topic->getId(), - $topic->getAttribute('name', ''), - $topic->getAttribute('description', ''), - ]; - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); - -Database::addFilter( - 'messageSearch', - function (mixed $value, Document $message) { - $searchValues = [ - $message->getId(), - $message->getAttribute('description', ''), - $message->getAttribute('status', ''), - ]; - - $data = \json_decode($message->getAttribute('data', []), true); - $providerType = $message->getAttribute('providerType', ''); - - if ($providerType === MESSAGE_TYPE_EMAIL) { - $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); - } elseif ($providerType === MESSAGE_TYPE_SMS) { - $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); - } else { - $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); - } - - $search = \implode(' ', \array_filter($searchValues)); - - return $search; - }, - function (mixed $value) { - return $value; - } -); diff --git a/app/init/database/formats.php b/app/init/database/formats.php deleted file mode 100644 index 88e46655ac..0000000000 --- a/app/init/database/formats.php +++ /dev/null @@ -1,43 +0,0 @@ -getScheme(); - $accessKey = $dsn->getUser() ?? ''; - $accessSecret = $dsn->getPassword() ?? ''; - $bucket = $dsn->getPath() ?? ''; - $region = $dsn->getParam('region'); - } catch (\Throwable $e) { - Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.'); - } - - switch ($device) { - case Storage::DEVICE_S3: - return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case STORAGE::DEVICE_DO_SPACES: - return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_BACKBLAZE: - return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LINODE: - return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_WASABI: - return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl); - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - } - } else { - switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) { - case Storage::DEVICE_LOCAL: - default: - return new Local($root); - case Storage::DEVICE_S3: - $s3AccessKey = System::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); - $s3SecretKey = System::getEnv('_APP_STORAGE_S3_SECRET', ''); - $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); - $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); - $s3Acl = 'private'; - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); - case Storage::DEVICE_DO_SPACES: - $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); - $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); - $doSpacesRegion = System::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''); - $doSpacesBucket = System::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''); - $doSpacesAcl = 'private'; - return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); - case Storage::DEVICE_BACKBLAZE: - $backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''); - $backblazeSecretKey = System::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''); - $backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''); - $backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''); - $backblazeAcl = 'private'; - return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl); - case Storage::DEVICE_LINODE: - $linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''); - $linodeSecretKey = System::getEnv('_APP_STORAGE_LINODE_SECRET', ''); - $linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', ''); - $linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', ''); - $linodeAcl = 'private'; - return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl); - case Storage::DEVICE_WASABI: - $wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''); - $wasabiSecretKey = System::getEnv('_APP_STORAGE_WASABI_SECRET', ''); - $wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', ''); - $wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', ''); - $wasabiAcl = 'private'; - return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl); - } - } -} - -$log = new Dependency(); -$mode = new Dependency(); -$user = new Dependency(); -$team = new Dependency(); -$plan = new Dependency(); -$pools = new Dependency(); -$geodb = new Dependency(); -$cache = new Dependency(); -$pools = new Dependency(); -$queue = new Dependency(); -$hooks = new Dependency(); -$logger = new Dependency(); -$locale = new Dependency(); -$schema = new Dependency(); -$github = new Dependency(); -$session = new Dependency(); -$console = new Dependency(); -$project = new Dependency(); -$clients = new Dependency(); -$servers = new Dependency(); -$register = new Dependency(); -$connections = new Dependency(); -$localeCodes = new Dependency(); -$getConsoleDB = new Dependency(); -$getProjectDB = new Dependency(); -$dbForProject = new Dependency(); -$dbForConsole = new Dependency(); -$queueForUsage = new Dependency(); -$queueForMails = new Dependency(); -$authorization = new Dependency(); -$authentication = new Dependency(); -$queueForBuilds = new Dependency(); -$deviceForLocal = new Dependency(); -$deviceForFiles = new Dependency(); -$queueForEvents = new Dependency(); -$queueForAudits = new Dependency(); -$promiseAdapter = new Dependency(); -$schemaVariable = new Dependency(); -$deviceForBuilds = new Dependency(); -$queueForDeletes = new Dependency(); -$requestTimestamp = new Dependency(); -$queueForDatabase = new Dependency(); -$queueForMessaging = new Dependency(); -$queueForFunctions = new Dependency(); -$queueForMigrations = new Dependency(); -$deviceForFunctions = new Dependency(); -$passwordsDictionary = new Dependency(); -$queueForCertificates = new Dependency(); - - -$team - ->setName('team') - ->inject('project') - ->inject('request') - ->inject('dbForConsole') - ->inject('authorization') - ->setCallback(function (Document $project, Request $request, Database $dbForConsole, Authorization $authorization) { - $teamInternalId = ''; - if ($project->getId() !== 'console') { - $teamInternalId = $project->getAttribute('teamInternalId', ''); - } else { - $method = (Http::REQUEST_METHOD_HEAD == $request->getMethod()) ? Http::REQUEST_METHOD_GET : $request->getMethod(); - $route = Router::match($method, $request->getURI()); - - if ($route !== null) { - $path = $route->getPath(); - if (str_starts_with($path, '/v1/projects/:projectId')) { - $uri = $request->getURI(); - $pid = explode('/', $uri)[3]; - $p = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $pid)); - $teamInternalId = $p->getAttribute('teamInternalId', ''); - } elseif ($path === '/v1/projects') { - $teamId = $request->getParam('teamId', ''); - $team = $authorization->skip(fn () => $dbForConsole->getDocument('teams', $teamId)); - return $team; - } - } - } - - $team = $authorization->skip(function () use ($dbForConsole, $teamInternalId) { - return $dbForConsole->findOne('teams', [ - Query::equal('$internalId', [$teamInternalId]), - ]); - }); - - if (!$team) { - $team = new Document([]); - } - return $team; - }); - - -$plan - ->setName('plan') - ->setCallback(fn () => []); - -$mode - ->setName('mode') - ->inject('request') - ->setCallback(function (Request $request) { - /** - * Defines the mode for the request: - * - 'default' => Requests for Client and Server Side - * - 'admin' => Request from the Console on non-console projects - */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); - }); - -$user - ->setName('user') - ->inject('mode') - ->inject('project') - ->inject('console') - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('dbForConsole') - ->inject('authorization') - ->inject('authentication') - ->setCallback(function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForConsole, Authorization $authorization, Authentication $authentication) { - $authorization->setDefaultStatus(true); - $authentication->setCookieName('a_session_' . $project->getId()); - - if (APP_MODE_ADMIN === $mode) { - $authentication->setCookieName('a_session_' . $console->getId()); - } - - $session = Auth::decodeSession( - $request->getCookie( - $authentication->getCookieName(), // Get sessions - $request->getCookie($authentication->getCookieName() . '_legacy', '') - ) - ); - - // Get session from header for SSR clients - if (empty($session['id']) && empty($session['secret'])) { - $sessionHeader = $request->getHeader('x-appwrite-session', ''); - - if (!empty($sessionHeader)) { - $session = Auth::decodeSession($sessionHeader); - } - } - - // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { - $response->addHeader('X-Debug-Fallback', 'false'); - } - - if (empty($session['id']) && empty($session['secret'])) { - if ($response) { - $response->addHeader('X-Debug-Fallback', 'true'); - } - $fallback = $request->getHeader('x-fallback-cookies', ''); - $fallback = \json_decode($fallback, true); - $session = Auth::decodeSession(((isset($fallback[$authentication->getCookieName()])) ? $fallback[$authentication->getCookieName()] : '')); - } - - $authentication->setUnique($session['id'] ?? ''); - $authentication->setSecret($session['secret'] ?? ''); - - if (APP_MODE_ADMIN !== $mode) { - if ($project->isEmpty()) { - $user = new Document([]); - } else { - if ($project->getId() === 'console') { - $user = $dbForConsole->getDocument('users', $authentication->getUnique()); - } else { - $user = $dbForProject->getDocument('users', $authentication->getUnique()); - } - } - } else { - $user = $dbForConsole->getDocument('users', $authentication->getUnique()); - } - - if ( - $user->isEmpty() // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) - ) { // Validate user has valid login token - $user = new Document([]); - } - - // if (APP_MODE_ADMIN === $mode) { - // if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) { - // $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users. - // } else { - // $user = new Document([]); - // } - // } - - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - - if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); - try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - $request->removeHeader('x-appwrite-jwt'); - throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); - } - - $jwtUserId = $payload['userId'] ?? ''; - if (!empty($jwtUserId)) { - $user = $dbForProject->getDocument('users', $jwtUserId); - } - - $jwtSessionId = $payload['sessionId'] ?? ''; - if (!empty($jwtSessionId)) { - if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new Document([]); - } - } - } - - // Adds logs to database queries - $dbForProject->setMetadata('user', $user->getId()); - $dbForConsole->setMetadata('user', $user->getId()); - - return $user; - }); - - -$session - ->setName('session') - ->inject('user') - ->inject('project') - ->inject('authorization') - ->inject('authentication') - ->setCallback(function (Document $user, Document $project, Authorization $authorization, Authentication $authentication) { - if ($user->isEmpty()) { - return; - } - - $sessions = $user->getAttribute('sessions', []); - $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $authentication->getSecret(), $authDuration); - - if (!$sessionId) { - return; - } - - foreach ($sessions as $session) { - if ($sessionId === $session->getId()) { - return $session; - } - } - - return; - }); - -$console - ->setName('console') - ->setCallback(function () { - return new Document([ - '$id' => ID::custom('console'), - '$internalId' => ID::custom('console'), - 'name' => 'Appwrite', - '$collection' => ID::custom('projects'), - 'description' => 'Appwrite core engine', - 'logo' => '', - 'teamId' => null, - 'webhooks' => [], - 'keys' => [], - 'platforms' => [ - [ - '$collection' => ID::custom('platforms'), - 'name' => 'Localhost', - 'type' => Origin::CLIENT_TYPE_WEB, - 'hostname' => 'localhost', - ], // Current host is added on app init - ], - 'legalName' => '', - 'legalCountry' => '', - 'legalState' => '', - 'legalCity' => '', - 'legalAddress' => '', - 'legalTaxId' => '', - 'auths' => [ - 'mockNumbers' => [], - 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', - 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds - 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled' - ], - 'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], - 'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'oAuthProviders' => [ - 'githubEnabled' => true, - 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), - 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') - ], - ]); - }); - - -$project - ->setName('project') - ->inject('dbForConsole') - ->inject('request') - ->inject('console') - ->inject('authorization') - ->setCallback(function (Database $dbForConsole, Request $request, Document $console, Authorization $authorization) { - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - - if (empty($projectId) || $projectId === 'console') { - return $console; - } - - $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); - - return $project; - }); - -$pools - ->setName('pools') - ->inject('registry') - ->setCallback(function (Registry $registry) { - return $registry->get('pools'); - }); - -$dbForProject - ->setName('dbForProject') - ->inject('cache') - ->inject('pools') - ->inject('project') - ->inject('dbForConsole') - ->inject('authorization') - ->inject('connections') - ->setCallback(function (Cache $cache, array $pools, Document $project, Database $dbForConsole, Authorization $authorization, Connections $connections) { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; - $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - $adapter = match ($connectionDsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - - $adapter->setDatabase($connectionDsn->getPath()); - - $database = new Database($adapter, $cache); - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if ($dsn->getHost() === DATABASE_SHARED_TABLES) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - $database->setAuthorization($authorization); - return $database; - }); - -$dbForConsole - ->setName('dbForConsole') - ->inject('getConsoleDB') - ->inject('connections') - ->setCallback(function (callable $getConsoleDB, Connections $connections): Database { - [$connection,$pool, $database] = $getConsoleDB(); - $connections->add($connection, $pool); - - return $database; - }); - -$cache - ->setName('cache') - ->inject('pools') - ->inject('connections') - ->setCallback(function (array $pools, Connections $connections) { - $adapters = []; - $databases = Config::getParam('pools-cache'); - - foreach ($databases as $database) { - $pool = $pools['pools-cache-' . $database]['pool']; - $dsn = $pools['pools-cache-' . $database]['dsn']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapters[] = new CacheRedis($connection); - } - - return new Cache(new Sharding($adapters)); - }); - -$authorization - ->setName('authorization') - ->setCallback(function (): Authorization { - return new Authorization(); - }); - -$authentication - ->setName('authentication') - ->setCallback(function (): Authentication { - return new Authentication(); - }); - -$register - ->setName('registry') - ->setCallback(function () use (&$registry): Registry { - return $registry; - }); - -$pools - ->setName('pools') - ->inject('registry') - ->setCallback(function (Registry $registry) { - return $registry->get('pools'); - }); - -$logger - ->setName('logger') - ->inject('registry') - ->setCallback(function (Registry $registry) { - return $registry->get('logger'); - }); - -$log - ->setName('log') - ->setCallback(function () { - return new Log(); - }); - -$connections - ->setName('connections') - ->setCallback(function () { - return new Connections(); - }); - -$locale - ->setName('locale') - ->setCallback(fn () => new Locale(System::getEnv('_APP_LOCALE', 'en'))); - -$localeCodes - ->setName('localeCodes') - ->setCallback(fn () => array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []))); - -$queue - ->setName('queue') - ->inject('pools') - ->inject('connections') - ->setCallback(function (array $pools, Connections $connections) { - $pool = $pools['pools-queue-queue']['pool']; - $connection = $pool->get(); - $connections->add($connection, $pool); - - return new Queue\Connection\Redis($connection); - }); - -$queueForMessaging - ->setName('queueForMessaging') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Messaging($queue); - }); - -$queueForMails - ->setName('queueForMails') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Mail($queue); - }); - -$queueForBuilds - ->setName('queueForBuilds') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Build($queue); - }); - -$queueForDatabase - ->setName('queueForDatabase') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new EventDatabase($queue); - }); - -$queueForDeletes - ->setName('queueForDeletes') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Delete($queue); - }); - -$queueForEvents - ->setName('queueForEvents') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Event($queue); - }); - -$queueForAudits - ->setName('queueForAudits') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Audit($queue); - }); - -$queueForFunctions - ->setName('queueForFunctions') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Func($queue); - }); - -$queueForUsage - ->setName('queueForUsage') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Usage($queue); - }); - -$queueForCertificates - ->setName('queueForCertificates') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Certificate($queue); - }); - -$queueForMigrations - ->setName('queueForMigrations') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new Migration($queue); - }); - -$deviceForLocal - ->setName('deviceForLocal') - ->setCallback(function () { - return new Local(); - }); - -$deviceForFiles - ->setName('deviceForFiles') - ->inject('project') - ->setCallback(function ($project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); - }); - -$deviceForFunctions - ->setName('deviceForFunctions') - ->inject('project') - ->setCallback(function ($project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); - }); - -$deviceForBuilds - ->setName('deviceForBuilds') - ->inject('project') - ->setCallback(function ($project) { - return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); - }); - -$clients - ->setName('clients') - ->inject('request') - ->inject('console') - ->inject('project') - ->setCallback(function (Request $request, Document $console, Document $project) { - $console->setAttribute('platforms', [ // Always allow current host - '$collection' => ID::custom('platforms'), - 'name' => 'Current Host', - 'type' => Origin::CLIENT_TYPE_WEB, - 'hostname' => $request->getHostname(), - ], Document::SET_TYPE_APPEND); - - $hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', '')); - $validator = new Hostname(); - foreach ($hostnames as $hostname) { - $hostname = trim($hostname); - if (!$validator->isValid($hostname)) { - continue; - } - $console->setAttribute('platforms', [ - '$collection' => ID::custom('platforms'), - 'type' => Origin::CLIENT_TYPE_WEB, - 'name' => $hostname, - 'hostname' => $hostname, - ], Document::SET_TYPE_APPEND); - } - - /** - * Get All verified client URLs for both console and current projects - * + Filter for duplicated entries - */ - $clientsConsole = \array_map( - fn ($node) => $node['hostname'], - \array_filter( - $console->getAttribute('platforms', []), - fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && isset($node['hostname']) && !empty($node['hostname'])) - ) - ); - - $clients = \array_unique( - \array_merge( - $clientsConsole, - \array_map( - fn ($node) => $node['hostname'], - \array_filter( - $project->getAttribute('platforms', []), - fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB || $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && isset($node['hostname']) && !empty($node['hostname'])) - ) - ) - ) - ); - - return $clients; - }); - -$servers - ->setName('servers') - ->setCallback(function () { - $platforms = Config::getParam('platforms'); - $server = $platforms[APP_PLATFORM_SERVER]; - - $languages = array_map(function ($language) { - return strtolower($language['name']); - }, $server['sdks']); - - return $languages; - }); - -$geodb - ->setName('geodb') - ->inject('registry') - ->setCallback(function (Registry $register) { - return $register->get('geodb'); - }); - -$passwordsDictionary - ->setName('passwordsDictionary') - ->setCallback(function () { - $content = file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords'); - $content = explode("\n", $content); - $content = array_flip($content); - return $content; - }); - -$hooks - ->setName('hooks') - ->inject('registry') - ->setCallback(function (Registry $registry) { - return $registry->get('hooks'); - }); - -$github - ->setName('gitHub') - ->inject('cache') - ->setCallback(function (Cache $cache) { - return new GitHub($cache); - }); - -$requestTimestamp - ->setName('requestTimestamp') - ->inject('request') - ->setCallback(function ($request) { - $timestampHeader = $request->getHeader('x-appwrite-timestamp'); - $requestTimestamp = null; - if (!empty($timestampHeader)) { - try { - $requestTimestamp = new \DateTime($timestampHeader); - } catch (\Throwable $e) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value'); - } - } - return $requestTimestamp; - }); -$getConsoleDB - ->setName('getConsoleDB') - ->inject('pools') - ->inject('cache') - ->inject('authorization') - ->inject('connections') - ->setCallback(function (array $pools, Cache $cache, Authorization $authorization) { - return function () use ($pools, $cache, $authorization): array { - $pool = $pools['pools-console-console']['pool']; - $dsn = $pools['pools-console-console']['dsn']; - $connection = $pool->get(); - - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - - $adapter->setDatabase($dsn->getPath()); - - $database = new Database($adapter, $cache); - $database->setAuthorization($authorization); - - $database - ->setNamespace('_console') - ->setMetadata('host', \gethostname()) - ->setMetadata('project', 'console') - ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); - - return [$connection, $pool, $database]; - }; - }); - -$getProjectDB - ->setName('getProjectDB') - ->inject('pools') - ->inject('dbForConsole') - ->inject('cache') - ->inject('authorization') - ->inject('connections') - ->setCallback(function (array $pools, Database $dbForConsole, Cache $cache, Authorization $authorization, Connections $connections) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools - - return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases, $authorization, $connections): Database { - if ($project->isEmpty() || $project->getId() === 'console') { - return $dbForConsole; - } - - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - - if (isset($databases[$dsn->getHost()])) { - $database = $databases[$dsn->getHost()]; - - if ($dsn->getHost() === DATABASE_SHARED_TABLES) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; - } - - $pool = $pools['pools-database-' . $dsn->getHost()]['pool']; - $connectionDsn = $pools['pools-database-' . $dsn->getHost()]['dsn']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - $adapter = match ($connectionDsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - $adapter->setDatabase($connectionDsn->getPath()); - - $database = new Database($adapter, $cache); - $database->setAuthorization($authorization); - - $databases[$dsn->getHost()] = $database; - - if ($dsn->getHost() === DATABASE_SHARED_TABLES) { - $database - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $database - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } - - return $database; - }; - }); - -$promiseAdapter - ->setName('promiseAdapter') - ->setCallback(function () use ($registry) { - return $registry->get('promiseAdapter'); - }); - -$schemaVariable - ->setName('schemaVariable') - ->setCallback(fn () => new Schema()); - -$schema - ->setName('schema') - ->inject('http') - ->inject('context') - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('authorization') - ->inject('schemaVariable') - ->setCallback(function (Http $http, Container $context, Request $request, Response $response, Database $dbForProject, Authorization $authorization, $schemaVariable) { - $complexity = function (int $complexity, array $args) { - $queries = Query::parseQueries($args['queries'] ?? []); - $query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null; - $limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT; - - return $complexity * $limit; - }; - - $attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) { - $attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [ - Query::limit($limit), - Query::offset($offset), - ])); - - return \array_map(function ($attr) { - return $attr->getArrayCopy(); - }, $attrs); - }; - - $urls = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents"; - }, - 'read' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - 'delete' => function (string $databaseId, string $collectionId, array $args) { - return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}"; - }, - ]; - - $params = [ - 'list' => function (string $databaseId, string $collectionId, array $args) { - return ['queries' => $args['queries']]; - }, - 'create' => function (string $databaseId, string $collectionId, array $args) { - $id = $args['id'] ?? 'unique()'; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - return [ - 'databaseId' => $databaseId, - 'documentId' => $id, - 'collectionId' => $collectionId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - 'update' => function (string $databaseId, string $collectionId, array $args) { - $documentId = $args['id']; - $permissions = $args['permissions'] ?? null; - - unset($args['id']); - unset($args['permissions']); - - return [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => $documentId, - 'data' => $args, - 'permissions' => $permissions, - ]; - }, - ]; - - return $schemaVariable->build( - $http, - $request, - $response, - $context, - $complexity, - $attributes, - $urls, - $params, - ); - }); - -$container->set($log); -$container->set($mode); -$container->set($user); -$container->set($team); -$container->set($plan); -$container->set($cache); -$container->set($pools); -$container->set($queue); -$container->set($geodb); -$container->set($hooks); -$container->set($locale); -$container->set($schema); -$container->set($github); -$container->set($logger); -$container->set($session); -$container->set($console); -$container->set($project); -$container->set($clients); -$container->set($servers); -$container->set($register); -$container->set($connections); -$container->set($localeCodes); -$container->set($dbForProject); -$container->set($dbForConsole); -$container->set($getConsoleDB); -$container->set($getProjectDB); -$container->set($queueForUsage); -$container->set($queueForMails); -$container->set($authorization); -$container->set($authentication); -$container->set($schemaVariable); -$container->set($queueForBuilds); -$container->set($queueForEvents); -$container->set($queueForAudits); -$container->set($deviceForLocal); -$container->set($deviceForFiles); -$container->set($promiseAdapter); -$container->set($queueForDeletes); -$container->set($deviceForBuilds); -$container->set($queueForDatabase); -$container->set($requestTimestamp); -$container->set($queueForMessaging); -$container->set($queueForFunctions); -$container->set($queueForMigrations); -$container->set($deviceForFunctions); -$container->set($passwordsDictionary); -$container->set($queueForCertificates); diff --git a/app/realtime.php b/app/realtime.php index 8f2b994b50..1b59eb3bc7 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,47 +5,136 @@ use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; -use Appwrite\Utopia\Queue\Connections; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Http\Request as SwooleRequest; -use Swoole\Http\Response as SwooleHttpResponse; use Swoole\Http\Response as SwooleResponse; use Swoole\Runtime; use Swoole\Table; use Swoole\Timer; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\Database\TimeLimit; +use Utopia\App; +use Utopia\Cache\Adapter\Sharding; +use Utopia\Cache\Cache; use Utopia\CLI\Console; +use Utopia\Config\Config; +use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -use Utopia\DI\Container; -use Utopia\DI\Dependency; -use Utopia\Http\Adapter\Swoole\Request as UtopiaRequest; -use Utopia\Http\Adapter\Swoole\Response as HttpResponse; -use Utopia\Http\Adapter\Swoole\Response as UtopiaResponse; -use Utopia\Http\Http; +use Utopia\Database\Validator\Authorization; +use Utopia\DSN\DSN; use Utopia\Logger\Log; -use Utopia\Pools\Connection; -use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\WebSocket\Adapter; use Utopia\WebSocket\Server; /** - * @var Registry $registry - * @var Container $container + * @var \Utopia\Registry\Registry $register */ -global $registry, $container; - - require_once __DIR__ . '/init.php'; Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +// Allows overriding +if (!function_exists('getConsoleDB')) { + function getConsoleDB(): Database + { + global $register; + + /** @var \Utopia\Pools\Group $pools */ + $pools = $register->get('pools'); + + $dbAdapter = $pools + ->get('console') + ->pop() + ->getResource() + ; + + $database = new Database($dbAdapter, getCache()); + + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', '_console'); + + return $database; + } +} + +// Allows overriding +if (!function_exists('getProjectDB')) { + function getProjectDB(Document $project): Database + { + global $register; + + /** @var \Utopia\Pools\Group $pools */ + $pools = $register->get('pools'); + + if ($project->isEmpty() || $project->getId() === 'console') { + return getConsoleDB(); + } + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $adapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); + + $database = new Database($adapter, getCache()); + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + $database + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()); + + return $database; + } +} + +// Allows overriding +if (!function_exists('getCache')) { + function getCache(): Cache + { + global $register; + + $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + + $list = Config::getParam('pools-cache', []); + $adapters = []; + + foreach ($list as $value) { + $adapters[] = $pools + ->get($value) + ->pop() + ->getResource() + ; + } + + return new Cache(new Sharding($adapters)); + } +} + if (!function_exists('getRealtime')) { function getRealtime(): Realtime { @@ -77,8 +166,8 @@ $adapter $server = new Server($adapter); -$logError = function (Throwable $error, string $action) use ($registry) { - $logger = $registry->get('logger'); +$logError = function (Throwable $error, string $action) use ($register) { + $logger = $register->get('logger'); if ($logger && !$error instanceof Exception) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); @@ -118,16 +207,16 @@ $logError = function (Throwable $error, string $action) use ($registry) { $server->error($logError); -$server->onStart(function () use ($stats, $container, $containerId, &$statsDocument, $logError) { +$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) { sleep(5); // wait for the initial database schema to be ready Console::success('Server started successfully'); - $authorization = $container->get('authorization'); + /** * Create document for this worker to share stats across Containers. */ - go(function () use ($container, $containerId, &$statsDocument) { + go(function () use ($register, $containerId, &$statsDocument) { $attempts = 0; - $database = $container->get('dbForConsole'); + $database = getConsoleDB(); do { try { @@ -141,15 +230,14 @@ $server->onStart(function () use ($stats, $container, $containerId, &$statsDocum 'value' => '{}' ]); - $authorization = $container->get('authorization'); - $statsDocument = $authorization->skip(fn () => $database->createDocument('realtime', $document)); + $statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document)); break; } catch (Throwable) { Console::warning("Collection not ready. Retrying connection ({$attempts})..."); sleep(DATABASE_RECONNECT_SLEEP); } } while (true); - ($container->get('connections'))->reclaim(); + $register->get('pools')->reclaim(); }); /** @@ -157,7 +245,7 @@ $server->onStart(function () use ($stats, $container, $containerId, &$statsDocum */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { - Timer::tick(5000, function () use ($container, $stats, &$statsDocument, $logError, $authorization) { + Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) { $payload = []; foreach ($stats as $projectId => $value) { $payload[$projectId] = $stats->get($projectId, 'connectionsTotal'); @@ -167,43 +255,40 @@ $server->onStart(function () use ($stats, $container, $containerId, &$statsDocum } try { - $database = $container->get('dbForConsole'); + $database = getConsoleDB(); $statsDocument ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - $authorization->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (Throwable $th) { call_user_func($logError, $th, "updateWorkerDocument"); } finally { - ($container->get('connections'))->reclaim(); - $container->refresh('dbForConsole'); + $register->get('pools')->reclaim(); } }); } }); -$server->onWorkerStart(function (int $workerId) use ($server, $container, $stats, $realtime, $logError) { +$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { Console::success('Worker ' . $workerId . ' started successfully'); $attempts = 0; $start = time(); - $authorization = $container->get('authorization'); - - Timer::tick(5000, function () use ($server, $container, $realtime, $stats, $logError, $authorization) { + Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) { /** * Sending current connections to project channels on the console project every 5 seconds. */ // TODO: Remove this if check once it doesn't cause issues for cloud if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') { if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) { - $database = $container->get('dbForConsole'); + $database = getConsoleDB(); $payload = []; - $list = $authorization->skip(fn () => $database->find('realtime', [ + $list = Authorization::skip(fn () => $database->find('realtime', [ Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)), ])); @@ -243,8 +328,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $container, $stats 'data' => $event['data'] ])); } - ($container->get('connections'))->reclaim(); - $container->refresh('dbForConsole'); + + $register->get('pools')->reclaim(); } } /** @@ -274,26 +359,13 @@ $server->onWorkerStart(function (int $workerId) use ($server, $container, $stats while ($attempts < 300) { try { if ($attempts > 0) { - Console::error( - 'Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). - Attempting restart in 5 seconds (attempt #' . $attempts . ')' - ); + Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). + Attempting restart in 5 seconds (attempt #' . $attempts . ')'); sleep(5); // 5 sec delay between connection attempts } - $start = time(); - $pools = $container->get('pools'); - $pool = $pools['pools-pubsub-pubsub']['pool']; - - /** @var Connections $connections */ - $connections = $container->get('connections'); - $connection = $pool->get(); - $connections->add($connection, $pool); - - $redis = $connection; - - /** @var Redis $redis */ + $redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */ $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -303,7 +375,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $container, $stats Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $realtime, $authorization, $container) { + $redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) { $event = json_decode($payload, true); if ($event['permissionsChanged'] && isset($event['userId'])) { @@ -312,24 +384,25 @@ $server->onWorkerStart(function (int $workerId) use ($server, $container, $stats if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) { $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); - $dbForConsole = $container->get('dbForConsole'); + $consoleDatabase = getConsoleDB(); + $project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); + $database = getProjectDB($project); - $project = $authorization->skip(fn () => $dbForConsole->getDocument('projects', $projectId)); - $dbForProject = $container->get('getProjectDB')($project); + $user = $database->getDocument('users', $userId); - $user = $dbForProject->getDocument('users', $userId); - - $roles = Auth::getRoles($user, $authorization); + $roles = Auth::getRoles($user); $channels = $realtime->connections[$connection]['channels']; $realtime->unsubscribe($connection); $realtime->subscribe($projectId, $connection, $roles, $channels); + + $register->get('pools')->reclaim(); } } $receivers = $realtime->getSubscribers($event); - if (Http::isDevelopment() && !empty($receivers)) { + if (App::isDevelopment() && !empty($receivers)) { Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers)); Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers)); Console::log("[Debug][Worker {$workerId}] Event: " . $payload); @@ -355,40 +428,30 @@ $server->onWorkerStart(function (int $workerId) use ($server, $container, $stats sleep(DATABASE_RECONNECT_SLEEP); continue; } finally { - ($container->get('connections'))->reclaim(); - $container->refresh('dbForConsole'); + $register->get('pools')->reclaim(); } } Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $container, $stats, &$realtime, $logError) { - $authorization = $container->get('authorization'); - - $request = new Request(new UtopiaRequest($request)); - $response = new Response(new UtopiaResponse(new SwooleResponse())); - - $requestInjection = new Dependency(); - $responseInjection = new Dependency(); - - $requestInjection->setName('request')->setCallback(fn () => $request); - $responseInjection->setName('response')->setCallback(fn () => $response); - - $container->set($requestInjection); - $container->set($responseInjection); +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) { + $app = new App('UTC'); + $request = new Request($request); + $response = new Response(new SwooleResponse()); Console::info("Connection open (user: {$connection})"); + App::setResource('pools', fn () => $register->get('pools')); + App::setResource('request', fn () => $request); + App::setResource('response', fn () => $response); + try { - /** @var Document $project */ - $project = $container->refresh('project')->get('project'); - - $container->refresh('dbForProject'); + $project = $app->getResource('project'); /* - * Project Check + * Project Check */ if (empty($project->getId())) { throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID'); @@ -397,16 +460,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, if ( array_key_exists('realtime', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['realtime'] - && !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles())) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } - $dbForProject = $container->get('getProjectDB')($project); - /** @var Document $console */ - $console = $container->get('console'); - /** @var Document $user */ - $user = $container->refresh('user')->get('user'); + $dbForProject = getProjectDB($project); + $console = $app->getResource('console'); /** @var Document $console */ + $user = $app->getResource('user'); /** @var Document $user */ + /* * Abuse Check * @@ -435,8 +497,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription()); } - $authorization = $container->get('authorization'); - $roles = Auth::getRoles($user, $authorization); + $roles = Auth::getRoles($user); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); @@ -485,33 +546,25 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $server->send([$connection], json_encode($response)); $server->close($connection, $code); - if (Http::isDevelopment()) { + if (App::isDevelopment()) { Console::error('[Error] Connection Error'); Console::error('[Error] Code: ' . $response['data']['code']); Console::error('[Error] Message: ' . $response['data']['message']); } } finally { - $connections = $container->get('connections'); - $connections->reclaim(); + $register->get('pools')->reclaim(); } }); -$server->onWorkerStop(function (int $workerId) use ($container) { - $connections = $container->get('connections'); - $connections->reclaim(); -}); - -$server->onMessage(function (int $connection, string $message) use ($server, $container, $realtime, $containerId) { +$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) { try { - $response = new Response(new HttpResponse(new SwooleHttpResponse())); + $response = new Response(new SwooleResponse()); $projectId = $realtime->connections[$connection]['projectId']; - $database = $container->get('dbForConsole'); - $authorization = $container->get('authorization'); - $authentication = $container->get('authentication'); + $database = getConsoleDB(); if ($projectId !== 'console') { - $project = $authorization->skip(fn () => $database->getDocument('projects', $projectId)); - $database = $container->get('getProjectDB')($project); + $project = Authorization::skip(fn () => $database->getDocument('projects', $projectId)); + $database = getProjectDB($project); } else { $project = null; } @@ -549,21 +602,20 @@ $server->onMessage(function (int $connection, string $message) use ($server, $co } $session = Auth::decodeSession($message['data']['session']); + Auth::$unique = $session['id'] ?? ''; + Auth::$secret = $session['secret'] ?? ''; - $authentication->setUnique($session['id'] ?? ''); - $authentication->setSecret($session['secret'] ?? ''); - - $user = $database->getDocument('users', $authentication->getUnique()); + $user = $database->getDocument('users', Auth::$unique); if ( empty($user->getId()) // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), $authentication->getSecret()) // Validate user has valid login token + || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token ) { // cookie not valid throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.'); } - $roles = Auth::getRoles($user, $authorization); + $roles = Auth::getRoles($user); $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); @@ -597,8 +649,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $co $server->close($connection, $th->getCode()); } } finally { - ($container->get('connections'))->reclaim(); - $container->refresh('dbForConsole'); + $register->get('pools')->reclaim(); } }); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 4365bf39fc..ad35135a6f 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -11,8 +11,7 @@ $httpsPort = $this->getParam('httpsPort', ''); $version = $this->getParam('version', ''); $organization = $this->getParam('organization', ''); $image = $this->getParam('image', ''); -?> -services: +?>services: traefik: image: traefik:2.11 container_name: appwrite-traefik @@ -854,7 +853,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' + command: 'mysqld --innodb-flush-method=fsync' redis: image: redis:7.2.4-alpine diff --git a/app/worker.php b/app/worker.php index 47093a31e1..2d59259284 100644 --- a/app/worker.php +++ b/app/worker.php @@ -2,107 +2,278 @@ require_once __DIR__ . '/init.php'; +use Appwrite\Event\Audit; +use Appwrite\Event\Build; +use Appwrite\Event\Certificate; +use Appwrite\Event\Database as EventDatabase; +use Appwrite\Event\Delete; +use Appwrite\Event\Event; +use Appwrite\Event\Func; +use Appwrite\Event\Mail; +use Appwrite\Event\Messaging; +use Appwrite\Event\Migration; +use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; use Appwrite\Platform\Appwrite; -use Appwrite\Utopia\Queue\Connections; use Swoole\Runtime; +use Utopia\App; +use Utopia\Cache\Adapter\Sharding; +use Utopia\Cache\Cache; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\DI\Dependency; +use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Platform\Service; +use Utopia\Pools\Group; use Utopia\Queue\Connection; use Utopia\Queue\Message; -use Utopia\Queue\Worker; -use Utopia\Storage\Device\Local; +use Utopia\Queue\Server; +use Utopia\Registry\Registry; use Utopia\System\System; -global $registry, $container; - +Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -$project = new Dependency(); -$register = new Dependency(); -$dbForProject = new Dependency(); -$abuseRetention = new Dependency(); -$deviceForCache = new Dependency(); -$auditRetention = new Dependency(); -$queueForUsageDump = new Dependency(); -$executionRetention = new Dependency(); -$deviceForLocalFiles = new Dependency(); +Server::setResource('register', fn () => $register); -$register - ->setName('register') - ->setCallback(fn () => $registry); +Server::setResource('dbForConsole', function (Cache $cache, Registry $register) { + $pools = $register->get('pools'); + $database = $pools + ->get('console') + ->pop() + ->getResource(); -$project - ->setName('project') - ->inject('message') - ->inject('dbForConsole') - ->setCallback(function (Message $message, Database $dbForConsole) { - $payload = $message->getPayload() ?? []; - $project = new Document($payload['project'] ?? []); + $adapter = new Database($database, $cache); + $adapter->setNamespace('_console'); - if ($project->getId() === 'console' || $project->isEmpty() || ! empty($project->getInternalId())) { - return $project; + return $adapter; +}, ['cache', 'register']); + +Server::setResource('project', function (Message $message, Database $dbForConsole) { + $payload = $message->getPayload() ?? []; + $project = new Document($payload['project'] ?? []); + + if ($project->getId() === 'console' || $project->isEmpty() || ! empty($project->getInternalId())) { + return $project; + } + + return $dbForConsole->getDocument('projects', $project->getId()); +}, ['message', 'dbForConsole']); + +Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; + } + + $pools = $register->get('pools'); + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + $adapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); + + $database = new Database($adapter, $cache); + + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } + + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + + return $database; +}, ['cache', 'register', 'message', 'project', 'dbForConsole']); + +Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) { + $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + + return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database { + if ($project->isEmpty() || $project->getId() === 'console') { + return $dbForConsole; } - return $dbForConsole->getDocument('projects', $project->getId()); - }); + try { + $dsn = new DSN($project->getAttribute('database')); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $project->getAttribute('database')); + } -$abuseRetention - ->setName('abuseRetention') - ->setCallback(function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); - }); + if (isset($databases[$dsn->getHost()])) { + $database = $databases[$dsn->getHost()]; -$auditRetention - ->setName('auditRetention') - ->setCallback(function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); - }); + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } -$executionRetention - ->setName('executionRetention') - ->setCallback(function () { - return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); - }); + return $database; + } -$queueForUsageDump - ->setName('queueForUsageDump') - ->inject('queue') - ->setCallback(function (Connection $queue) { - return new UsageDump($queue); - }); + $dbAdapter = $pools + ->get($dsn->getHost()) + ->pop() + ->getResource(); -$deviceForCache - ->setName('deviceForCache') - ->inject('project') - ->setCallback(function (Document $project) { - return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); - }); + $database = new Database($dbAdapter, $cache); -$deviceForLocalFiles - ->setName('deviceForLocalFiles') - ->inject('project') - ->setCallback(function (Document $project) { - return new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); - }); + $databases[$dsn->getHost()] = $database; -$container->set($project); -$container->set($register); -$container->set($dbForProject); -$container->set($abuseRetention); -$container->set($auditRetention); -$container->set($deviceForCache); -$container->set($queueForUsageDump); -$container->set($executionRetention); -$container->set($deviceForLocalFiles); + if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $database + ->setSharedTables(true) + ->setTenant($project->getInternalId()) + ->setNamespace($dsn->getParam('namespace')); + } else { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); + } + return $database; + }; +}, ['pools', 'dbForConsole', 'cache']); + +Server::setResource('abuseRetention', function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400)); +}); + +Server::setResource('auditRetention', function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); +}); + +Server::setResource('executionRetention', function () { + return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); +}); + +Server::setResource('cache', function (Registry $register) { + $pools = $register->get('pools'); + $list = Config::getParam('pools-cache', []); + $adapters = []; + + foreach ($list as $value) { + $adapters[] = $pools + ->get($value) + ->pop() + ->getResource() + ; + } + + return new Cache(new Sharding($adapters)); +}, ['register']); + +Server::setResource('log', fn () => new Log()); + +Server::setResource('queueForUsage', function (Connection $queue) { + return new Usage($queue); +}, ['queue']); + +Server::setResource('queueForUsageDump', function (Connection $queue) { + return new UsageDump($queue); +}, ['queue']); + +Server::setResource('queue', function (Group $pools) { + return $pools->get('queue')->pop()->getResource(); +}, ['pools']); + +Server::setResource('queueForDatabase', function (Connection $queue) { + return new EventDatabase($queue); +}, ['queue']); + +Server::setResource('queueForMessaging', function (Connection $queue) { + return new Messaging($queue); +}, ['queue']); + +Server::setResource('queueForMails', function (Connection $queue) { + return new Mail($queue); +}, ['queue']); + +Server::setResource('queueForBuilds', function (Connection $queue) { + return new Build($queue); +}, ['queue']); + +Server::setResource('queueForDeletes', function (Connection $queue) { + return new Delete($queue); +}, ['queue']); + +Server::setResource('queueForEvents', function (Connection $queue) { + return new Event($queue); +}, ['queue']); + +Server::setResource('queueForAudits', function (Connection $queue) { + return new Audit($queue); +}, ['queue']); + +Server::setResource('queueForFunctions', function (Connection $queue) { + return new Func($queue); +}, ['queue']); + +Server::setResource('queueForCertificates', function (Connection $queue) { + return new Certificate($queue); +}, ['queue']); + +Server::setResource('queueForMigrations', function (Connection $queue) { + return new Migration($queue); +}, ['queue']); + +Server::setResource('logger', function (Registry $register) { + return $register->get('logger'); +}, ['register']); + +Server::setResource('pools', function (Registry $register) { + return $register->get('pools'); +}, ['register']); + +Server::setResource('deviceForFunctions', function (Document $project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); +}, ['project']); + +Server::setResource('deviceForFiles', function (Document $project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); +}, ['project']); + +Server::setResource('deviceForBuilds', function (Document $project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); +}, ['project']); + +Server::setResource('deviceForCache', function (Document $project) { + return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); +}, ['project']); + + +$pools = $register->get('pools'); $platform = new Appwrite(); $args = $platform->getEnv('argv'); @@ -121,13 +292,6 @@ if (\str_starts_with($workerName, 'databases')) { } try { - $connection = new Connection\Redis( - System::getEnv('_APP_REDIS_HOST', 'redis'), - System::getEnv('_APP_REDIS_PORT', '6379'), - System::getEnv('_APP_REDIS_USER', ''), - System::getEnv('_APP_REDIS_PASS', '') - ); - /** * Any worker can be configured with the following env vars: * - _APP_WORKERS_NUM The total number of worker processes @@ -136,35 +300,32 @@ try { */ $platform->init(Service::TYPE_WORKER, [ 'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1), - 'connection' => $connection, + 'connection' => $pools->get('queue')->pop()->getResource(), 'workerName' => strtolower($workerName) ?? null, 'queueName' => $queueName ]); } catch (\Throwable $e) { - Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); + Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); } -Worker::init() - ->inject('authorization') - ->action(function (Authorization $authorization) { - $authorization->disable(); +$worker = $platform->getWorker(); + +$worker + ->shutdown() + ->inject('pools') + ->action(function (Group $pools) { + $pools->reclaim(); }); -Worker::shutdown() - ->inject('connections') - ->action(function (Connections $connections) { - $connections->reclaim(); - }); - -Worker::error() +$worker + ->error() ->inject('error') ->inject('logger') ->inject('log') - ->inject('connections') + ->inject('pools') ->inject('project') - ->inject('authorization') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Connections $connections, Document $project, Authorization $authorization) use ($queueName) { - $connections->reclaim(); + ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) { + $pools->reclaim(); $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { @@ -180,7 +341,7 @@ Worker::error() $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', $authorization->getRoles()); + $log->addExtra('roles', Authorization::getRoles()); $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); @@ -199,7 +360,9 @@ Worker::error() Console::error('[Error] Line: ' . $error->getLine()); }); -$platform - ->getWorker() - ->setContainer($container) - ->start(); +$worker->workerStart() + ->action(function () use ($workerName) { + Console::info("Worker $workerName started"); + }); + +$worker->start(); diff --git a/composer.json b/composer.json index 135822bc9e..c176d383f3 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,6 @@ "description": "End to end backend server for frontend and mobile apps.", "type": "project", "license": "BSD-3-Clause", - "minimum-stability": "stable", "authors": [ { "name": "Eldad Fux", @@ -15,8 +14,7 @@ "test": "vendor/bin/phpunit", "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "bench": "vendor/bin/phpbench run --report=benchmark", - "check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G app src tests" + "bench": "vendor/bin/phpbench run --report=benchmark" }, "autoload": { "psr-4": { @@ -47,33 +45,33 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.15.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.46.*", - "utopia-php/analytics": "0.13.*", - "utopia-php/audit": "0.46.*", + "utopia-php/abuse": "0.43.0", + "utopia-php/analytics": "0.10.*", + "utopia-php/audit": "0.43.0", "utopia-php/cache": "0.10.*", - "utopia-php/cli": "0.19.*", + "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.55.1", - "utopia-php/domains": "0.6.*", - "utopia-php/dsn": "0.2.*", - "utopia-php/framework": "1.0.*", + "utopia-php/database": "0.53.5", + "utopia-php/domains": "0.5.*", + "utopia-php/dsn": "0.2.1", + "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.2.*", "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.12.*", - "utopia-php/migration": "0.7.*", - "utopia-php/orchestration": "0.15.*", - "utopia-php/platform": "0.8.*", - "utopia-php/view": "0.2.*", + "utopia-php/migration": "0.6.*", + "utopia-php/orchestration": "0.9.*", + "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.5.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.8.*", + "utopia-php/queue": "0.7.*", "utopia-php/registry": "0.5.*", - "utopia-php/storage": "0.19.*", + "utopia-php/storage": "0.18.*", + "utopia-php/swoole": "0.8.*", "utopia-php/system": "0.8.*", - "utopia-php/vcs": "0.9.*", - "utopia-php/websocket": "0.2.*", + "utopia-php/vcs": "0.8.*", + "utopia-php/websocket": "0.1.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.9.1", @@ -90,8 +88,7 @@ "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.7", "laravel/pint": "^1.14", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "1.8.*" + "phpbench/phpbench": "^1.2" }, "provide": { "ext-phpiredis": "*" diff --git a/composer.lock b/composer.lock index cb468c1663..d2c82a65d7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b808f001c3ff4c0396cf51f46e8b314a", + "content-hash": "1884e3a2966762c4a955842426b64f6c", "packages": [ { "name": "adhocore/jwt", @@ -1430,16 +1430,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.46.0", + "version": "0.43.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3" + "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/ab09ecf6ceac5420020e6046ec651c155005d3c3", - "reference": "ab09ecf6ceac5420020e6046ec651c155005d3c3", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6", + "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6", "shasum": "" }, "require": { @@ -1447,7 +1447,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.55.*" + "utopia-php/database": "0.53.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1475,28 +1475,27 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.46.0" + "source": "https://github.com/utopia-php/abuse/tree/0.43.0" }, - "time": "2024-10-07T19:09:29+00:00" + "time": "2024-08-30T05:17:23+00:00" }, { "name": "utopia-php/analytics", - "version": "0.13.0", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/utopia-php/analytics.git", - "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4" + "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/analytics/zipball/3dace02af5d4190623f88fb6e02f5559a99f14c4", - "reference": "3dace02af5d4190623f88fb6e02f5559a99f14c4", + "url": "https://api.github.com/repos/utopia-php/analytics/zipball/14c805114736f44c26d6d24b176a2f8b93d86a1f", + "reference": "14c805114736f44c26d6d24b176a2f8b93d86a1f", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.19.*", - "utopia-php/system": "0.8.*" + "utopia-php/cli": "^0.15.0" }, "require-dev": { "laravel/pint": "dev-main", @@ -1522,27 +1521,27 @@ ], "support": { "issues": "https://github.com/utopia-php/analytics/issues", - "source": "https://github.com/utopia-php/analytics/tree/0.13.0" + "source": "https://github.com/utopia-php/analytics/tree/0.10.2" }, - "time": "2024-09-05T16:19:26+00:00" + "time": "2023-03-22T12:01:09+00:00" }, { "name": "utopia-php/audit", - "version": "0.46.0", + "version": "0.43.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d" + "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/660bb322ca1e6a9fa121610a85930e65863e1e5d", - "reference": "660bb322ca1e6a9fa121610a85930e65863e1e5d", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", + "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.55.*" + "utopia-php/database": "0.53.*" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1569,9 +1568,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.46.0" + "source": "https://github.com/utopia-php/audit/tree/0.43.0" }, - "time": "2024-10-07T19:10:12+00:00" + "time": "2024-08-30T05:17:36+00:00" }, { "name": "utopia-php/cache", @@ -1625,29 +1624,27 @@ }, { "name": "utopia-php/cli", - "version": "0.19.0", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "f8af1d6087f498bc1f0191750a118d357ded9948" + "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/f8af1d6087f498bc1f0191750a118d357ded9948", - "reference": "f8af1d6087f498bc1f0191750a118d357ded9948", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", + "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/di": "0.1.*", - "utopia-php/framework": "1.0.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.6", - "swoole/ide-helper": "4.8.8" + "vimeo/psalm": "4.0.1" }, "type": "library", "autoload": { @@ -1670,9 +1667,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.19.0" + "source": "https://github.com/utopia-php/cli/tree/0.15.0" }, - "time": "2024-09-05T15:46:56+00:00" + "time": "2023-03-01T05:55:14+00:00" }, { "name": "utopia-php/config", @@ -1727,16 +1724,16 @@ }, { "name": "utopia-php/database", - "version": "0.55.1", + "version": "0.53.5", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828" + "reference": "689ba22063bf46def385da8695ba7a921e81e38d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/6f44079999491feb0ef85686f6b2ac7ef54db828", - "reference": "6f44079999491feb0ef85686f6b2ac7ef54db828", + "url": "https://api.github.com/repos/utopia-php/database/zipball/689ba22063bf46def385da8695ba7a921e81e38d", + "reference": "689ba22063bf46def385da8695ba7a921e81e38d", "shasum": "" }, "require": { @@ -1744,7 +1741,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.10.*", - "utopia-php/framework": "1.0.*", + "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1755,7 +1752,7 @@ "phpunit/phpunit": "9.6.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.19.*" + "utopia-php/cli": "0.14.*" }, "type": "library", "autoload": { @@ -1777,79 +1774,30 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.55.1" + "source": "https://github.com/utopia-php/database/tree/0.53.5" }, - "time": "2024-10-07T18:52:26+00:00" - }, - { - "name": "utopia-php/di", - "version": "0.1.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/di.git", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "laravel/pint": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25", - "swoole/ide-helper": "4.8.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\": "src/", - "Tests\\E2E\\": "tests/e2e" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A simple and lite library for managing dependency injections", - "keywords": [ - "framework", - "http", - "php", - "upf" - ], - "support": { - "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.1.0" - }, - "time": "2024-08-08T14:35:19+00:00" + "time": "2024-09-24T08:43:10+00:00" }, { "name": "utopia-php/domains", - "version": "0.6.0", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda" + "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/5c70b0f524deeb1fccc3962ad1da650ae2c94cda", - "reference": "5c70b0f524deeb1fccc3962ad1da650ae2c94cda", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c", + "reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/framework": "1.0.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.3" }, "type": "library", @@ -1886,9 +1834,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.6.0" + "source": "https://github.com/utopia-php/domains/tree/0.5.0" }, - "time": "2024-09-05T16:21:11+00:00" + "time": "2024-01-03T22:04:27+00:00" }, { "name": "utopia-php/dsn", @@ -1978,30 +1926,26 @@ }, { "name": "utopia-php/framework", - "version": "1.0.2", + "version": "0.33.8", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "fc63ec61c720190a5ea5bb484c615145850951e6" + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/fc63ec61c720190a5ea5bb484c615145850951e6", - "reference": "fc63ec61c720190a5ea5bb484c615145850951e6", + "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", "shasum": "" }, "require": { - "ext-swoole": "*", - "php": ">=8.0", - "utopia-php/servers": "0.1.*" + "php": ">=8.0" }, "require-dev": { - "ext-xdebug": "*", "laravel/pint": "^1.2", "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25", - "swoole/ide-helper": "4.8.3" + "phpunit/phpunit": "^9.5.25" }, "type": "library", "autoload": { @@ -2013,18 +1957,17 @@ "license": [ "MIT" ], - "description": "A simple, light and advanced PHP HTTP framework", + "description": "A simple, light and advanced PHP framework", "keywords": [ "framework", - "http", "php", "upf" ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/1.0.2" + "source": "https://github.com/utopia-php/http/tree/0.33.8" }, - "time": "2024-09-10T09:04:19+00:00" + "time": "2024-08-15T14:10:09+00:00" }, { "name": "utopia-php/image", @@ -2232,16 +2175,16 @@ }, { "name": "utopia-php/migration", - "version": "0.7.0", + "version": "0.6.4", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084" + "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/aa996ebfd3223fca2aa7d8b9aa31457c95344084", - "reference": "aa996ebfd3223fca2aa7d8b9aa31457c95344084", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", + "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", "shasum": "" }, "require": { @@ -2249,16 +2192,17 @@ "ext-curl": "*", "ext-openssl": "*", "php": "8.3.*", - "utopia-php/database": "0.55.*", + "utopia-php/database": "0.53.*", "utopia-php/dsn": "0.2.*", - "utopia-php/storage": "0.19.*" + "utopia-php/framework": "0.33.*", + "utopia-php/storage": "0.18.*" }, "require-dev": { "ext-pdo": "*", "laravel/pint": "1.17.*", "phpstan/phpstan": "1.11.*", "phpunit/phpunit": "11.2.*", - "utopia-php/cli": "0.19.*", + "utopia-php/cli": "0.16.*", "vlucas/phpdotenv": "5.6.*" }, "type": "library", @@ -2281,9 +2225,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.7.0" + "source": "https://github.com/utopia-php/migration/tree/0.6.4" }, - "time": "2024-10-07T19:00:18+00:00" + "time": "2024-10-02T15:16:36+00:00" }, { "name": "utopia-php/mongo", @@ -2347,26 +2291,26 @@ }, { "name": "utopia-php/orchestration", - "version": "0.15.0", + "version": "0.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/orchestration.git", - "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3" + "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/cd55650ba5f13118c3580048e6dd86b604f9a5b3", - "reference": "cd55650ba5f13118c3580048e6dd86b604f9a5b3", + "url": "https://api.github.com/repos/utopia-php/orchestration/zipball/55f43513b3f940a3f4f9c2cde7682d0c2581beb0", + "reference": "55f43513b3f940a3f4f9c2cde7682d0c2581beb0", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.19.*" + "utopia-php/cli": "0.15.*" }, "require-dev": { "laravel/pint": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" }, "type": "library", "autoload": { @@ -2391,32 +2335,31 @@ ], "support": { "issues": "https://github.com/utopia-php/orchestration/issues", - "source": "https://github.com/utopia-php/orchestration/tree/0.15.0" + "source": "https://github.com/utopia-php/orchestration/tree/0.9.1" }, - "time": "2024-09-05T16:28:02+00:00" + "time": "2023-03-17T15:05:06+00:00" }, { "name": "utopia-php/platform", - "version": "0.8.1", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "95d57f38a4001c7189a66885c485ac635d305234" + "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/95d57f38a4001c7189a66885c485ac635d305234", - "reference": "95d57f38a4001c7189a66885c485ac635d305234", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/cli": "0.19.*", - "utopia-php/framework": "1.0.*", - "utopia-php/queue": "0.8.*", - "utopia-php/servers": "0.1.*" + "utopia-php/cli": "0.15.*", + "utopia-php/framework": "0.33.*", + "utopia-php/queue": "0.7.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2442,9 +2385,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.8.1" + "source": "https://github.com/utopia-php/platform/tree/0.7.0" }, - "time": "2024-09-06T02:33:27+00:00" + "time": "2024-05-08T17:00:55+00:00" }, { "name": "utopia-php/pools", @@ -2552,23 +2495,22 @@ }, { "name": "utopia-php/queue", - "version": "0.8.0", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "a518b271f8c158d6e66e36972f767189111033c2" + "reference": "917565256eb94bcab7246f7a746b1a486813761b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/a518b271f8c158d6e66e36972f767189111033c2", - "reference": "a518b271f8c158d6e66e36972f767189111033c2", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/917565256eb94bcab7246f7a746b1a486813761b", + "reference": "917565256eb94bcab7246f7a746b1a486813761b", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/cli": "0.19.*", - "utopia-php/di": "0.1.*", - "utopia-php/servers": "0.1.*" + "utopia-php/cli": "0.15.*", + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "^0.2.3", @@ -2578,7 +2520,6 @@ "workerman/workerman": "^4.0" }, "suggest": { - "ext-redis": "Needed to support Redis connections", "ext-swoole": "Needed to support Swoole.", "workerman/workerman": "Needed to support Workerman." }, @@ -2609,9 +2550,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.8.0" + "source": "https://github.com/utopia-php/queue/tree/0.7.0" }, - "time": "2024-09-05T16:33:01+00:00" + "time": "2024-01-17T19:00:43+00:00" }, { "name": "utopia-php/registry", @@ -2665,71 +2606,18 @@ }, "time": "2021-03-10T10:45:22+00:00" }, - { - "name": "utopia-php/servers", - "version": "0.1.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/servers.git", - "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/fd5c8d32778f265256c1936372a071b944f5ba8a", - "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a", - "shasum": "" - }, - "require": { - "php": ">=8.0", - "utopia-php/di": "0.1.*" - }, - "require-dev": { - "laravel/pint": "^0.2.3", - "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^9.5.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Servers\\": "src/Servers" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Team Appwrite", - "email": "team@appwrite.io" - } - ], - "description": "A base library for building Utopia style servers.", - "keywords": [ - "framework", - "php", - "servers", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.1.1" - }, - "time": "2024-09-06T02:25:56+00:00" - }, { "name": "utopia-php/storage", - "version": "0.19.0", + "version": "0.18.5", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "5013b894a776874d6010753fc9349df2225c69af" + "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/5013b894a776874d6010753fc9349df2225c69af", - "reference": "5013b894a776874d6010753fc9349df2225c69af", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/7d355c5e3ccc8ecebc0266f8ddd30088a43be919", + "reference": "7d355c5e3ccc8ecebc0266f8ddd30088a43be919", "shasum": "" }, "require": { @@ -2741,8 +2629,8 @@ "ext-zlib": "*", "ext-zstd": "*", "php": ">=8.0", - "utopia-php/framework": "1.0.*", - "utopia-php/system": "0.8.*" + "utopia-php/framework": "0.*.*", + "utopia-php/system": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2769,9 +2657,60 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.19.0" + "source": "https://github.com/utopia-php/storage/tree/0.18.5" }, - "time": "2024-09-05T17:00:24+00:00" + "time": "2024-09-04T08:57:27+00:00" + }, + { + "name": "utopia-php/swoole", + "version": "0.8.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/swoole.git", + "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", + "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", + "shasum": "" + }, + "require": { + "ext-swoole": "*", + "php": ">=8.0", + "utopia-php/framework": "0.33.*" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3", + "swoole/ide-helper": "5.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Swoole\\": "src/Swoole" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "An extension for Utopia Framework to work with PHP Swoole as a PHP FPM alternative", + "keywords": [ + "framework", + "http", + "php", + "server", + "swoole", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/swoole/issues", + "source": "https://github.com/utopia-php/swoole/tree/0.8.2" + }, + "time": "2024-02-01T14:54:12+00:00" }, { "name": "utopia-php/system", @@ -2831,24 +2770,23 @@ }, { "name": "utopia-php/vcs", - "version": "0.9.0", + "version": "0.8.2", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7" + "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/673abe2fef0750a841a4fa8fa6f99d4a602c68e7", - "reference": "673abe2fef0750a841a4fa8fa6f99d4a602c68e7", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", + "reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18", "shasum": "" }, "require": { "adhocore/jwt": "^1.1", "php": ">=8.0", - "utopia-php/cache": "0.10.*", - "utopia-php/framework": "1.0.*", - "utopia-php/system": "0.8.*" + "utopia-php/cache": "^0.10.0", + "utopia-php/framework": "0.*.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -2875,76 +2813,32 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.9.0" + "source": "https://github.com/utopia-php/vcs/tree/0.8.2" }, - "time": "2024-09-05T16:44:48+00:00" - }, - { - "name": "utopia-php/view", - "version": "0.2.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/view.git", - "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/view/zipball/6ee55e83bc014c39ed6b69390f6d399116f65e88", - "reference": "6ee55e83bc014c39ed6b69390f6d399116f65e88", - "shasum": "" - }, - "require": { - "php": ">=8.0" - }, - "require-dev": { - "laravel/pint": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\View\\": "src/View" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A simple, light and advanced PHP rendering engine", - "keywords": [ - "php", - "view" - ], - "support": { - "issues": "https://github.com/utopia-php/view/issues", - "source": "https://github.com/utopia-php/view/tree/0.2.0" - }, - "time": "2024-04-01T17:21:29+00:00" + "time": "2024-08-13T14:36:30+00:00" }, { "name": "utopia-php/websocket", - "version": "0.2.0", + "version": "0.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "e9d0919b321744a61f12563f5791c47ba9f57810" + "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/e9d0919b321744a61f12563f5791c47ba9f57810", - "reference": "e9d0919b321744a61f12563f5791c47ba9f57810", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", + "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", "shasum": "" }, "require": { "php": ">=8.0" }, "require-dev": { - "laravel/pint": "^1.15", - "phpstan/phpstan": "^1.8", "phpunit/phpunit": "^9.5.5", - "swoole/ide-helper": "5.1.2", + "swoole/ide-helper": "4.6.6", "textalk/websocket": "1.5.2", + "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, "type": "library", @@ -2957,6 +2851,16 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + }, + { + "name": "Torsten Dittmann", + "email": "torsten@appwrite.io" + } + ], "description": "A simple abstraction for WebSocket servers.", "keywords": [ "framework", @@ -2967,9 +2871,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.2.0" + "source": "https://github.com/utopia-php/websocket/tree/0.1.0" }, - "time": "2024-04-09T08:28:11+00:00" + "time": "2021-12-20T10:50:09+00:00" }, { "name": "webmozart/assert", @@ -4335,65 +4239,6 @@ }, "time": "2024-09-26T07:23:32+00:00" }, - { - "name": "phpstan/phpstan", - "version": "1.8.11", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "46e223dd68a620da18855c23046ddb00940b4014" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", - "reference": "46e223dd68a620da18855c23046ddb00940b4014", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" - }, - "bin": [ - "phpstan", - "phpstan.phar" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.11" - }, - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], - "time": "2022-10-24T15:45:13+00:00" - }, { "name": "phpunit/php-code-coverage", "version": "9.2.32", diff --git a/docker-compose.yml b/docker-compose.yml index c4bdffa1d0..6ecb0ecff8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -682,7 +682,6 @@ services: entrypoint: maintenance <<: *x-logging container_name: appwrite-task-maintenance - restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -783,7 +782,6 @@ services: entrypoint: schedule-functions <<: *x-logging container_name: appwrite-task-scheduler-functions - restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -812,7 +810,6 @@ services: entrypoint: schedule-executions <<: *x-logging container_name: appwrite-task-scheduler-executions - restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -840,7 +837,6 @@ services: entrypoint: schedule-messages <<: *x-logging container_name: appwrite-task-scheduler-messages - restart: unless-stopped image: appwrite-dev networks: - appwrite @@ -960,7 +956,7 @@ services: - MYSQL_USER=${_APP_DB_USER} - MYSQL_PASSWORD=${_APP_DB_PASS} - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' + command: "mysqld --innodb-flush-method=fsync" redis: image: redis:7.2.4-alpine diff --git a/docs/tutorials/add-route.md b/docs/tutorials/add-route.md index 0baa51b5c0..ac6fd40bdb 100644 --- a/docs/tutorials/add-route.md +++ b/docs/tutorials/add-route.md @@ -8,7 +8,7 @@ Setting an alias allows the route to be also accessible from the alias URL. The first parameter specifies the alias URL, the second parameter specifies default values for route parameters. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->alias('/v1/storage/files', ['bucketId' => 'default']) ``` @@ -17,7 +17,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') Used as an abstract description of the route. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->desc('Create File') ``` @@ -26,14 +26,14 @@ Http::post('/v1/storage/buckets/:bucketId/files') Groups array is used to group one or more routes with one or more hooks functionality. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->groups(['api']) ``` In the above example groups() is used to define the current route as part of the routes that shares a common init middleware hook. ```php -Http::init() +App::init() ->groups(['api']) ->action( some code..... @@ -52,7 +52,7 @@ Appwrite uses different labels to achieve different things, for example: - scope - Defines the route permissions scope. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->label('scope', 'files.write') ``` @@ -66,7 +66,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') - audits.resource - Signals the extraction part of the resource. ```php -Http::post('/v1/account/create') +App::post('/v1/account/create') ->label('audits.event', 'account.create') ->label('audits.resource', 'user/{response.$id}') ->label('audits.userId', '{response.$id}') @@ -84,7 +84,7 @@ Http::post('/v1/account/create') * sdk.offline.response.key - JSON property name that has the ID. Defaults to $id ```php -Http::post('/v1/account/jwt') +App::post('/v1/account/jwt') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createJWT') @@ -100,7 +100,7 @@ Http::post('/v1/account/jwt') - cache.resource - Identifies the cached resource. ```php -Http::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') +App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->label('cache', true) ->label('cache.resource', 'file/{request.fileId}') ``` @@ -115,7 +115,7 @@ When using the example below, we configure the abuse mechanism to allow this key constructed from the combination of the ip, http method, url, userId to hit the route maximum 60 times in 1 hour (60 seconds \* 60 minutes). ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}') ->label('abuse-limit', 60) ->label('abuse-time', 3600) @@ -127,7 +127,7 @@ Http::post('/v1/storage/buckets/:bucketId/files') Placeholders marked as `[]` are parsed and replaced with their real values. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->label('event', 'buckets.[bucketId].files.[fileId].create') ``` @@ -145,7 +145,7 @@ As the name implies, `param()` is used to define a request parameter. - An array of injections ```php -Http::get('/v1/account/logs') +App::get('/v1/account/logs') ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ``` @@ -154,14 +154,14 @@ Http::get('/v1/account/logs') inject is used to inject dependencies pre-bounded to the app. ```php -Http::post('/v1/storage/buckets/:bucketId/files') +App::post('/v1/storage/buckets/:bucketId/files') ->inject('user') ``` -In the example above, the user object is injected into the route pre-bounded using `Http::setResource()`. +In the example above, the user object is injected into the route pre-bounded using `App::setResource()`. ```php -Http::setResource('user', function() { +App::setResource('user', function() { some code... }); ``` @@ -170,7 +170,7 @@ some code... Action populates the actual route code and has to be very clear and understandable. A good route stays simple and doesn't contain complex logic. An action is where we describe our business needs in code, and combine different libraries to work together and tell our story. ```php -Http::post('/v1/account/sessions/anonymous') +App::post('/v1/account/sessions/anonymous') ->action(function (Request $request) { some code... }); diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index 25771ef17c..0000000000 --- a/phpstan.neon +++ /dev/null @@ -1,11 +0,0 @@ -parameters: - level: 0 - scanDirectories: - - vendor/swoole/ide-helper - excludePaths: - - tests/resources - ignoreErrors: - - '#Parameter \$geodb of anonymous function has invalid type MaxMind\\Db\\Reader\.#' - - '#Parameter \$geodb of function router\(\) has invalid type MaxMind\\Db\\Reader\.#' - - '#Instantiated class MaxMind\\Db\\Reader not found.#' - - '#Function scrypt not found\.#' diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 0135020765..1e8109622e 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -91,6 +91,37 @@ class Auth */ public const MFA_RECENT_DURATION = 1800; // 30 mins + /** + * @var string + */ + public static $cookieName = 'a_session'; + + /** + * User Unique ID. + * + * @var string + */ + public static $unique = ''; + + /** + * User Secret Key. + * + * @var string + */ + public static $secret = ''; + + /** + * Set Cookie Name. + * + * @param $string + * + * @return string + */ + public static function setCookieName($string) + { + return self::$cookieName = $string; + } + /** * Encode Session. * @@ -408,14 +439,13 @@ class Auth * Returns all roles for a user. * * @param Document $user - * @param Authorization $auth * @return array */ - public static function getRoles(Document $user, Authorization $auth): array + public static function getRoles(Document $user): array { $roles = []; - if (!self::isPrivilegedUser($auth->getRoles()) && !self::isAppUser($auth->getRoles())) { + if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) { if ($user->getId()) { $roles[] = Role::user($user->getId())->toString(); $roles[] = Role::users()->toString(); diff --git a/src/Appwrite/Auth/Authentication.php b/src/Appwrite/Auth/Authentication.php deleted file mode 100644 index ef372309da..0000000000 --- a/src/Appwrite/Auth/Authentication.php +++ /dev/null @@ -1,57 +0,0 @@ -cookieName = $string; - } - - public function getCookieName(): string - { - return $this->cookieName; - } - - public function getUnique(): string - { - return $this->unique; - } - - public function setUnique(string $unique): void - { - $this->unique = $unique; - } - - public function getSecret(): string - { - return $this->secret; - } - - public function setSecret(string $secret): void - { - $this->secret = $secret; - } - -} diff --git a/src/Appwrite/Auth/Validator/MockNumber.php b/src/Appwrite/Auth/Validator/MockNumber.php index 8f0f14c9da..ac5ba89fc5 100644 --- a/src/Appwrite/Auth/Validator/MockNumber.php +++ b/src/Appwrite/Auth/Validator/MockNumber.php @@ -2,8 +2,8 @@ namespace Appwrite\Auth\Validator; -use Utopia\Http\Validator; -use Utopia\Http\Validator\Text; +use Utopia\Validator; +use Utopia\Validator\Text; /** * MockNumber. @@ -52,7 +52,6 @@ class MockNumber extends Validator return false; } - $this->message = ''; return true; } diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index 913701f7a3..bfe5577889 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; /** * Password. diff --git a/src/Appwrite/Auth/Validator/Phone.php b/src/Appwrite/Auth/Validator/Phone.php index d5f6df60c8..26aa687278 100644 --- a/src/Appwrite/Auth/Validator/Phone.php +++ b/src/Appwrite/Auth/Validator/Phone.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; /** * Phone. diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index 2fbdb7e785..0cbaf17b60 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -175,9 +175,9 @@ class Func extends Event * * @return string */ - public function getBody(): string + public function getData(): string { - return $this->body; + return $this->data; } /** diff --git a/src/Appwrite/Event/Validator/Event.php b/src/Appwrite/Event/Validator/Event.php index 2d46bd7727..a3605e4df5 100644 --- a/src/Appwrite/Event/Validator/Event.php +++ b/src/Appwrite/Event/Validator/Event.php @@ -3,7 +3,7 @@ namespace Appwrite\Event\Validator; use Utopia\Config\Config; -use Utopia\Http\Validator; +use Utopia\Validator; class Event extends Validator { diff --git a/src/Appwrite/Functions/Validator/Headers.php b/src/Appwrite/Functions/Validator/Headers.php index 4c30a98045..04003d535b 100644 --- a/src/Appwrite/Functions/Validator/Headers.php +++ b/src/Appwrite/Functions/Validator/Headers.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; /** * Headers. diff --git a/src/Appwrite/Functions/Validator/Payload.php b/src/Appwrite/Functions/Validator/Payload.php index 3b2ff4b918..acb461eabd 100644 --- a/src/Appwrite/Functions/Validator/Payload.php +++ b/src/Appwrite/Functions/Validator/Payload.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Text; class Payload extends Text { diff --git a/src/Appwrite/Functions/Validator/RuntimeSpecification.php b/src/Appwrite/Functions/Validator/RuntimeSpecification.php index fa6efe90a4..22311838f6 100644 --- a/src/Appwrite/Functions/Validator/RuntimeSpecification.php +++ b/src/Appwrite/Functions/Validator/RuntimeSpecification.php @@ -2,7 +2,7 @@ namespace Appwrite\Functions\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; class RuntimeSpecification extends Validator { diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 49d7c421f7..8bc72af2f8 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -6,12 +6,9 @@ use Appwrite\GraphQL\Exception as GQLException; use Appwrite\Promises\Swoole; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Utopia\DI\Container; +use Utopia\App; use Utopia\Exception; -use Utopia\Http\Http; -use Utopia\Http\Request as UtopiaHttpRequest; -use Utopia\Http\Response as UtopiaHttpResponse; -use Utopia\Http\Route; +use Utopia\Route; use Utopia\System\System; class Resolvers @@ -19,21 +16,24 @@ class Resolvers /** * Create a resolver for a given API {@see Route}. * - * @param Http $http + * @param App $utopia * @param ?Route $route * @return callable */ - public function api( - Http $http, + public static function api( + App $utopia, ?Route $route, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) { + /** @var App $utopia */ + /** @var Response $response */ + /** @var Request $request */ + + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $route, $args, $context, $container, $info, $request, $response, $resolver) { $path = $route->getPath(); foreach ($args as $key => $value) { if (\str_contains($path, '/:' . $key)) { @@ -46,13 +46,14 @@ class Resolvers switch ($route->getMethod()) { case 'GET': - $request->setQuery($args); + $request->setQueryString($args); break; default: $request->setPayload($args); break; } - $resolver->resolve($http, $request, $response, $container, $resolve, $reject); + + self::resolve($utopia, $request, $response, $resolve, $reject); } ); } @@ -60,20 +61,20 @@ class Resolvers /** * Create a resolver for a document in a specified database and collection with a specific method type. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param string $methodType * @return callable */ - public function document( - Http $http, + public static function document( + App $utopia, string $databaseId, string $collectionId, string $methodType, ): callable { return [self::class, 'document' . \ucfirst($methodType)]( - $http, + $utopia, $databaseId, $collectionId ); @@ -82,28 +83,28 @@ class Resolvers /** * Create a resolver for getting a document in a specified database and collection. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public function documentGet( - Http $http, + public static function documentGet( + App $utopia, string $databaseId, string $collectionId, callable $url, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); + $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - $resolver->resolve($http, $request, $response, $container, $resolve, $reject); + self::resolve($utopia, $request, $response, $resolve, $reject); } ); } @@ -111,35 +112,35 @@ class Resolvers /** * Create a resolver for listing documents in a specified database and collection. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public function documentList( - Http $http, + public static function documentList( + App $utopia, string $databaseId, string $collectionId, callable $url, callable $params, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); + $request->setMethod('GET'); $request->setURI($url($databaseId, $collectionId, $args)); - $request->setQuery($params($databaseId, $collectionId, $args)); + $request->setQueryString($params($databaseId, $collectionId, $args)); $beforeResolve = function ($payload) { return $payload['documents']; }; - $resolver->resolve($http, $request, $response, $container, $resolve, $reject, $beforeResolve); + self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve); } ); } @@ -147,31 +148,31 @@ class Resolvers /** * Create a resolver for creating a document in a specified database and collection. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public function documentCreate( - Http $http, + public static function documentCreate( + App $utopia, string $databaseId, string $collectionId, callable $url, callable $params, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); + $request->setMethod('POST'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - $resolver->resolve($http, $request, $response, $container, $resolve, $reject); + self::resolve($utopia, $request, $response, $resolve, $reject); } ); } @@ -179,31 +180,31 @@ class Resolvers /** * Create a resolver for updating a document in a specified database and collection. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param callable $url * @param callable $params * @return callable */ - public function documentUpdate( - Http $http, + public static function documentUpdate( + App $utopia, string $databaseId, string $collectionId, callable $url, callable $params, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $params, $type, $args, $container, $request, $response, $resolver) { + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) { + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); + $request->setMethod('PATCH'); $request->setURI($url($databaseId, $collectionId, $args)); $request->setPayload($params($databaseId, $collectionId, $args)); - $resolver->resolve($http, $request, $response, $container, $resolve, $reject); + self::resolve($utopia, $request, $response, $resolve, $reject); } ); } @@ -211,34 +212,34 @@ class Resolvers /** * Create a resolver for deleting a document in a specified database and collection. * - * @param Http $http + * @param App $utopia * @param string $databaseId * @param string $collectionId * @param callable $url * @return callable */ - public function documentDelete( - Http $http, + public static function documentDelete( + App $utopia, string $databaseId, string $collectionId, callable $url, - UtopiaHttpRequest $request, - UtopiaHttpResponse $response, - Container $container, ): callable { - $resolver = $this; - return fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($http, $databaseId, $collectionId, $url, $type, $args, $container, $request, $response, $resolver) { + return static fn ($type, $args, $context, $info) => new Swoole( + function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) { + $utopia = $utopia->getResource('utopia:graphql', true); + $request = $utopia->getResource('request', true); + $response = $utopia->getResource('response', true); + $request->setMethod('DELETE'); $request->setURI($url($databaseId, $collectionId, $args)); - $resolver->resolve($http, $request, $response, $container, $resolve, $reject); + self::resolve($utopia, $request, $response, $resolve, $reject); } ); } /** - * @param Http $http + * @param App $utopia * @param Request $request * @param Response $response * @param callable $resolve @@ -248,11 +249,10 @@ class Resolvers * @return void * @throws Exception */ - private function resolve( - Http $http, + private static function resolve( + App $utopia, Request $request, Response $response, - Container $context, callable $resolve, callable $reject, ?callable $beforeResolve = null, @@ -263,16 +263,14 @@ class Resolvers $request->removeHeader('content-type'); } + $request = clone $request; + $utopia->setResource('request', static fn () => $request); $response->setContentType(Response::CONTENT_TYPE_NULL); try { - $context - ->refresh('cache') - ->refresh('dbForProject') - ->refresh('dbForConsole') - ->refresh('getProjectDb'); + $route = $utopia->match($request, fresh: true); - $http->run(clone $context); + $utopia->execute($route, $request, $response); } catch (\Throwable $e) { if ($beforeReject) { $e = $beforeReject($e); @@ -287,12 +285,10 @@ class Resolvers if ($beforeReject) { $payload = $beforeReject($payload); } - $reject( - new GQLException( - message: $payload['message'], - code: $response->getStatusCode() - ) - ); + $reject(new GQLException( + message: $payload['message'], + code: $response->getStatusCode() + )); return; } diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php index 2b05f08aee..833ea9d032 100644 --- a/src/Appwrite/GraphQL/Schema.php +++ b/src/Appwrite/GraphQL/Schema.php @@ -3,63 +3,54 @@ namespace Appwrite\GraphQL; use Appwrite\GraphQL\Types\Mapper; -use Appwrite\Utopia\Response\Models; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema as GQLSchema; -use Utopia\DI\Container; +use Utopia\App; use Utopia\Exception; -use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; -use Utopia\Http\Http; -use Utopia\Http\Request; -use Utopia\Http\Response as UtopiaHttpResponse; -use Utopia\Http\Route; +use Utopia\Route; class Schema { - protected ?GQLSchema $schema = null; - protected array $dirty = []; + protected static ?GQLSchema $schema = null; + protected static array $dirty = []; /** * - * @param Http $http - * @param callable $complexity Function to calculate complexity - * @param callable $attributes Function to get attributes - * @param array $urls Array of functions to get urls for specific method types - * @param array $params Array of functions to build parameters for specific method types + * @param App $utopia + * @param callable $complexity Function to calculate complexity + * @param callable $attributes Function to get attributes + * @param array $urls Array of functions to get urls for specific method types + * @param array $params Array of functions to build parameters for specific method types * @return GQLSchema * @throws Exception */ - public function build( - Http $http, - Request $request, - UtopiaHttpResponse $response, - Container $container, + public static function build( + App $utopia, callable $complexity, callable $attributes, array $urls, array $params, ): GQLSchema { - if (!empty($this->schema)) { - return $this->schema; + App::setResource('utopia:graphql', static function () use ($utopia) { + return $utopia; + }); + + if (!empty(self::$schema)) { + return self::$schema; } - $api = $this->api( - $http, - $request, - $response, - $container, + $api = static::api( + $utopia, $complexity ); - // $collections = $this->collections( - // $http, - // $complexity, - // $request, - // $response, - // $attributes, - // $urls, - // $params, - // ); + //$collections = static::collections( + // $utopia, + // $complexity, + // $attributes, + // $urls, + // $params, + //); $queries = \array_merge_recursive( $api['query'], @@ -73,7 +64,7 @@ class Schema \ksort($queries); \ksort($mutations); - return $this->schema = new GQLSchema([ + return static::$schema = new GQLSchema([ 'query' => new ObjectType([ 'name' => 'Query', 'fields' => $queries @@ -89,23 +80,21 @@ class Schema * This function iterates all API routes and builds a GraphQL * schema defining types and resolvers for all response models. * - * @param Http $http - * @param Request $request - * @param UtopiaSwooleResponse $response + * @param App $utopia * @param callable $complexity * @return array - * @throws \Exception + * @throws Exception */ - protected function api(Http $http, Request $request, UtopiaHttpResponse $response, Container $container, callable $complexity): array + protected static function api(App $utopia, callable $complexity): array { - Mapper::init(Models::getModels()); - - $mapper = new Mapper(); + Mapper::init($utopia + ->getResource('response') + ->getModels()); $queries = []; $mutations = []; - foreach ($http->getRoutes() as $routes) { + foreach ($utopia->getRoutes() as $routes) { foreach ($routes as $route) { /** @var Route $route */ @@ -117,7 +106,7 @@ class Schema continue; } - foreach ($mapper->route($http, $route, $request, $response, $container, $complexity) as $field) { + foreach (Mapper::route($utopia, $route, $complexity) as $field) { switch ($route->getMethod()) { case 'GET': $queries[$name] = $field; @@ -145,7 +134,7 @@ class Schema * Iterates all of a projects attributes and builds GraphQL * queries and mutations for the collections they make up. * - * @param Http $http + * @param App $utopia * @param callable $complexity * @param callable $attributes * @param array $urls @@ -154,7 +143,7 @@ class Schema * @throws \Exception */ protected static function collections( - Http $http, + App $utopia, callable $complexity, callable $attributes, array $urls, @@ -205,36 +194,36 @@ class Schema $queryFields[$collectionId . 'Get'] = [ 'type' => $objectType, 'args' => Mapper::args('id'), - /*'resolve' => Resolvers::documentGet( - $http, + 'resolve' => Resolvers::documentGet( + $utopia, $databaseId, $collectionId, $urls['get'], - )*/ + ) ]; $queryFields[$collectionId . 'List'] = [ 'type' => Type::listOf($objectType), 'args' => Mapper::args('list'), - /*'resolve' => Resolvers::documentList( - $http, + 'resolve' => Resolvers::documentList( + $utopia, $databaseId, $collectionId, $urls['list'], $params['list'], - ),*/ + ), 'complexity' => $complexity, ]; $mutationFields[$collectionId . 'Create'] = [ 'type' => $objectType, 'args' => $attributes, - /*'resolve' => Resolvers::documentCreate( - $http, + 'resolve' => Resolvers::documentCreate( + $utopia, $databaseId, $collectionId, $urls['create'], $params['create'], - )*/ + ) ]; $mutationFields[$collectionId . 'Update'] = [ 'type' => $objectType, @@ -245,23 +234,23 @@ class Schema $attributes ) ), - /*'resolve' => Resolvers::documentUpdate( - $http, + 'resolve' => Resolvers::documentUpdate( + $utopia, $databaseId, $collectionId, $urls['update'], $params['update'], - )*/ + ) ]; $mutationFields[$collectionId . 'Delete'] = [ 'type' => Mapper::model('none'), 'args' => Mapper::args('id'), - /*'resolve' => Resolvers::documentDelete( - $http, + 'resolve' => Resolvers::documentDelete( + $utopia, $databaseId, $collectionId, $urls['delete'], - )*/ + ) ]; } $offset += $limit; @@ -273,8 +262,8 @@ class Schema ]; } - public function setDirty(string $projectId): void + public static function setDirty(string $projectId): void { - $this->dirty[$projectId] = true; + self::$dirty[$projectId] = true; } } diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index d4478e3301..d8f1d7da09 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -8,13 +8,10 @@ use Exception; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use Utopia\DI\Container; -use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; -use Utopia\Http\Http; -use Utopia\Http\Request; -use Utopia\Http\Route; -use Utopia\Http\Validator; -use Utopia\Http\Validator\Nullable; +use Utopia\App; +use Utopia\Route; +use Utopia\Validator; +use Utopia\Validator\Nullable; class Mapper { @@ -78,15 +75,12 @@ class Mapper return self::$args[$key] ?? []; } - public function route( - Http $http, + public static function route( + App $utopia, Route $route, - Request $request, - UtopiaSwooleResponse $response, - Container $container, callable $complexity ): iterable { - foreach (static::$blacklist as $blacklist) { + foreach (self::$blacklist as $blacklist) { if (\str_starts_with($route->getPath(), $blacklist)) { return; } @@ -108,7 +102,7 @@ class Mapper $list = true; } $parameterType = Mapper::param( - $container, + $utopia, $parameter['validator'], !$parameter['optional'], $parameter['injections'] @@ -123,7 +117,7 @@ class Mapper 'type' => $type, 'description' => $description, 'args' => $params, - 'resolve' => (new Resolvers())->api($http, $route, $request, $response, $container) + 'resolve' => Resolvers::api($utopia, $route) ]; if ($list) { @@ -212,7 +206,7 @@ class Mapper /** * Map a {@see Route} parameter to a GraphQL Type * - * @param Container $container + * @param App $utopia * @param Validator|callable $validator * @param bool $required * @param array $injections @@ -220,13 +214,13 @@ class Mapper * @throws Exception */ public static function param( - Container $container, + App $utopia, Validator|callable $validator, bool $required, array $injections ): Type { $validator = \is_callable($validator) - ? \call_user_func_array($validator, array_map(fn ($injection) => $container->get($injection), $injections)) + ? \call_user_func_array($validator, $utopia->getResources($injections)) : $validator; $isNullable = $validator instanceof Nullable; @@ -239,20 +233,20 @@ class Mapper case 'Appwrite\Network\Validator\CNAME': case 'Appwrite\Task\Validator\Cron': case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Utopia\Http\Validator\Domain': + case 'Utopia\Validator\Domain': case 'Appwrite\Network\Validator\Email': case 'Appwrite\Event\Validator\Event': case 'Appwrite\Event\Validator\FunctionEvent': - case 'Utopia\Http\Validator\HexColor': - case 'Utopia\Http\Validator\Host': - case 'Utopia\Http\Validator\IP': + case 'Utopia\Validator\HexColor': + case 'Utopia\Validator\Host': + case 'Utopia\Validator\IP': case 'Utopia\Database\Validator\Key': - case 'Utopia\Http\Validator\Origin': + case 'Utopia\Validator\Origin': case 'Appwrite\Auth\Validator\Password': - case 'Utopia\Http\Validator\Text': + case 'Utopia\Validator\Text': case 'Utopia\Database\Validator\UID': - case 'Utopia\Http\Validator\URL': - case 'Utopia\Http\Validator\WhiteList': + case 'Utopia\Validator\URL': + case 'Utopia\Validator\WhiteList': default: $type = Type::string(); break; @@ -280,29 +274,29 @@ class Mapper case 'Appwrite\Utopia\Database\Validator\Queries\Variables': $type = Type::listOf(Type::string()); break; - case 'Utopia\Http\Validator\Boolean': + case 'Utopia\Validator\Boolean': $type = Type::boolean(); break; - case 'Utopia\Http\Validator\ArrayList': + case 'Utopia\Validator\ArrayList': $type = Type::listOf(self::param( - $container, + $utopia, $validator->getValidator(), $required, $injections )); break; - case 'Utopia\Http\Validator\Integer': - case 'Utopia\Http\Validator\Numeric': - case 'Utopia\Http\Validator\Range': + case 'Utopia\Validator\Integer': + case 'Utopia\Validator\Numeric': + case 'Utopia\Validator\Range': $type = Type::int(); break; - case 'Utopia\Http\Validator\FloatValidator': + case 'Utopia\Validator\FloatValidator': $type = Type::float(); break; - case 'Utopia\Http\Validator\Assoc': + case 'Utopia\Validator\Assoc': $type = Types::assoc(); break; - case 'Utopia\Http\Validator\JSON': + case 'Utopia\Validator\JSON': $type = Types::json(); break; case 'Utopia\Storage\Validator\File': diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 5b215ec04f..cee1b2d263 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -98,10 +98,10 @@ abstract class Migration */ protected array $collections; - public function __construct(Authorization $auth) + public function __construct() { - $auth->disable(); - $auth->setDefaultStatus(false); + Authorization::disable(); + Authorization::setDefaultStatus(false); $this->collections = Config::getParam('collections', []); @@ -176,23 +176,25 @@ abstract class Migration Console::log('Migrating Collection ' . $collection['$id'] . ':'); foreach ($this->documentsIterator($collection['$id']) as $document) { - if (empty($document->getId()) || empty($document->getCollection())) { - return; - } + go(function (Document $document, callable $callback) { + if (empty($document->getId()) || empty($document->getCollection())) { + return; + } - $old = $document->getArrayCopy(); - $new = call_user_func($callback, $document); + $old = $document->getArrayCopy(); + $new = call_user_func($callback, $document); - if (is_null($new) || $new->getArrayCopy() == $old) { - return; - } + if (is_null($new) || $new->getArrayCopy() == $old) { + return; + } - try { - $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); - } catch (\Throwable $th) { - Console::error('Failed to update document: ' . $th->getMessage()); - return; - } + try { + $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document); + } catch (\Throwable $th) { + Console::error('Failed to update document: ' . $th->getMessage()); + return; + } + }, $document, $callback); } } } diff --git a/src/Appwrite/Network/Validator/CNAME.php b/src/Appwrite/Network/Validator/CNAME.php index e9e2b586a5..e1ae061c84 100644 --- a/src/Appwrite/Network/Validator/CNAME.php +++ b/src/Appwrite/Network/Validator/CNAME.php @@ -2,7 +2,7 @@ namespace Appwrite\Network\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; class CNAME extends Validator { diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php index bae0ff0bbf..3209a4aada 100644 --- a/src/Appwrite/Network/Validator/Email.php +++ b/src/Appwrite/Network/Validator/Email.php @@ -2,14 +2,14 @@ namespace Appwrite\Network\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; /** * Email * * Validate that an variable is a valid email address * - * @package Utopia\Http\Validator + * @package Utopia\Validator */ class Email extends Validator { diff --git a/src/Appwrite/Network/Validator/Origin.php b/src/Appwrite/Network/Validator/Origin.php index 573a59b844..d41e9af2ad 100644 --- a/src/Appwrite/Network/Validator/Origin.php +++ b/src/Appwrite/Network/Validator/Origin.php @@ -2,8 +2,8 @@ namespace Appwrite\Network\Validator; -use Utopia\Http\Validator; -use Utopia\Http\Validator\Hostname; +use Utopia\Validator; +use Utopia\Validator\Hostname; class Origin extends Validator { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 3bf9e0d33b..82d1ca2d59 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -3,17 +3,13 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; -use Appwrite\Utopia\Queue\Connections; +use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Database\Adapter\MariaDB; -use Utopia\Database\Adapter\MySQL; use Utopia\Domains\Domain; use Utopia\DSN\DSN; -use Utopia\Http\Http; use Utopia\Logger\Logger; use Utopia\Platform\Action; -use Utopia\Queue\Connection\Redis; use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; @@ -31,11 +27,10 @@ class Doctor extends Action $this ->desc('Validate server health') ->inject('register') - ->inject('connections') - ->callback(fn (Registry $register, Connections $connections) => $this->action($register, $connections)); + ->callback(fn (Registry $register) => $this->action($register)); } - public function action(Registry $register, Connections $connections): void + public function action(Registry $register): void { Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ @@ -130,73 +125,17 @@ class Doctor extends Action //throw $th; } - /** @var array $pools */ - $pools = $register->get('pools'); + $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ $configs = [ - 'Console.DB' => [ - 'prefix' => 'console', - 'databases' => Config::getParam('pools-console') - ], - 'Database.DB' => [ - 'prefix' => 'database', - 'databases' => Config::getParam('pools-database') - ], + 'Console.DB' => Config::getParam('pools-console'), + 'Projects.DB' => Config::getParam('pools-database'), ]; - foreach ($configs as $key => $config) { - foreach ($config['databases'] as $database) { + foreach ($config as $database) { try { - $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; - $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; - - $connection = $pool->get(); - $connections->add($connection, $pool); - $adapter = match ($dsn->getScheme()) { - 'mariadb' => new MariaDB($connection), - 'mysql' => new MySQL($connection), - default => null - }; - $adapter->setDatabase($dsn->getPath()); - - - if ($adapter->ping()) { - Console::success('🟢 ' . str_pad("$key({$database})", 50, '.') . 'connected'); - } else { - Console::error('🔴 ' . str_pad("$key({$database})", 47, '.') . 'disconnected'); - } - } catch (\Throwable $th) { - Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected'); - } - } - } - - /** @var array $pools */ - $pools = $register->get('pools'); - $configs = [ - 'Cache' => [ - 'prefix' => 'cache', - 'databases' => Config::getParam('pools-cache') - ], - 'Queue' => [ - 'prefix' => 'queue', - 'databases' => Config::getParam('pools-queue') - ], - 'PubSub' => [ - 'prefix' => 'pubsub', - 'databases' => Config::getParam('pools-pubsub') - ], - ]; - foreach ($configs as $key => $config) { - foreach ($config['databases'] as $database) { - try { - $pool = $pools['pools-' . $config['prefix'] . '-' . $database]['pool']; - $dsn = $pools['pools-' . $config['prefix'] . '-' . $database]['dsn']; - $connection = $pool->get(); - $connections->add($connection, $pool); - - $adapter = new Redis($dsn->getHost(), $dsn->getPort()); + $adapter = $pools->get($database)->pop()->getResource(); if ($adapter->ping()) { Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); @@ -204,7 +143,30 @@ class Doctor extends Action Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } } catch (\Throwable $th) { - Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); + Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected'); + } + } + } + + $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + $configs = [ + 'Cache' => Config::getParam('pools-cache'), + 'Queue' => Config::getParam('pools-queue'), + 'PubSub' => Config::getParam('pools-pubsub'), + ]; + + foreach ($configs as $key => $config) { + foreach ($config as $pool) { + try { + $adapter = $pools->get($pool)->pop()->getResource(); + + if ($adapter->ping()) { + Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected'); + } else { + Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); + } + } catch (\Throwable $th) { + Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); } } } @@ -296,7 +258,7 @@ class Doctor extends Action } try { - if (Http::isProduction()) { + if (App::isProduction()) { Console::log(''); $version = \json_decode(@\file_get_contents(System::getEnv('_APP_HOME', 'http://localhost') . '/version'), true); diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index a1a73e6385..4abd267684 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -8,9 +8,9 @@ use Appwrite\Docker\Env; use Appwrite\Utopia\View; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Text; use Utopia\Platform\Action; +use Utopia\Validator\Boolean; +use Utopia\Validator\Text; class Install extends Action { @@ -213,7 +213,8 @@ class Install extends Action } $env = ''; - $output = ''; + $stdout = ''; + $stderr = ''; foreach ($input as $key => $value) { if ($value) { @@ -224,13 +225,13 @@ class Install extends Action $exit = 0; if (!$noStart) { Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); - $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $output); + $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); } if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; Console::error($message); - Console::error($output); + Console::error($stderr); Console::exit($exit); } else { $message = 'Appwrite installed successfully'; diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index 55be0b81c2..dcba59bb1d 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -10,10 +10,10 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Http\Validator\Text; use Utopia\Platform\Action; use Utopia\Registry\Registry; use Utopia\System\System; +use Utopia\Validator\Text; class Migrate extends Action { @@ -30,15 +30,12 @@ class Migrate extends Action ->desc('Migrate Appwrite to new version') /** @TODO APP_VERSION_STABLE needs to be defined */ ->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true) - ->inject('cache') ->inject('dbForConsole') ->inject('getProjectDB') ->inject('register') - ->inject('authorization') - ->inject('console') - ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register, Authorization $authorization, Document $console) { - \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register, $authorization, $console) { - $this->action($version, $dbForConsole, $getProjectDB, $register, $authorization, $console); + ->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) { + \Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) { + $this->action($version, $dbForConsole, $getProjectDB, $register); }); }); } @@ -61,12 +58,13 @@ class Migrate extends Action } } - public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register, Authorization $auth, Document $console) + public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register) { - $auth->disable(); + Authorization::disable(); if (!array_key_exists($version, Migration::$versions)) { Console::error("Version {$version} not found."); Console::exit(1); + return; } @@ -79,8 +77,12 @@ class Migrate extends Action 10 ); + $app = new App('UTC'); + Console::success('Starting Data Migration to version ' . $version); + $console = $app->getResource('console'); + $limit = 30; $sum = 30; $offset = 0; @@ -99,7 +101,7 @@ class Migrate extends Action $class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version]; /** @var Migration $migration */ - $migration = new $class($auth, ); + $migration = new $class(); while (!empty($projects)) { foreach ($projects as $project) { diff --git a/src/Appwrite/Platform/Tasks/QueueCount.php b/src/Appwrite/Platform/Tasks/QueueCount.php index 63f829073a..b02165c1d2 100644 --- a/src/Appwrite/Platform/Tasks/QueueCount.php +++ b/src/Appwrite/Platform/Tasks/QueueCount.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; class QueueCount extends Action { diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index 63f6c8e11e..b6139dc177 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -3,11 +3,11 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\Wildcard; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; +use Utopia\Validator\Text; +use Utopia\Validator\Wildcard; class QueueRetry extends Action { diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index ad4098d9ee..5af0cb6cd8 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -5,10 +5,10 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Certificate; use Utopia\CLI\Console; use Utopia\Database\Document; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Hostname; use Utopia\Platform\Action; use Utopia\System\System; +use Utopia\Validator\Boolean; +use Utopia\Validator\Hostname; class SSL extends Action { diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index c2636f82e6..a1b85c341f 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -2,45 +2,41 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Utopia\Queue\Connections; use Swoole\Timer; use Utopia\CLI\Console; +use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; use Utopia\Platform\Action; +use Utopia\Pools\Group; use Utopia\System\System; +use function Swoole\Coroutine\run; + abstract class ScheduleBase extends Action { protected const UPDATE_TIMER = 10; //seconds protected const ENQUEUE_TIMER = 60; //seconds protected array $schedules = []; - protected Connections $connections; abstract public static function getName(): string; - abstract public static function getSupportedResource(): string; abstract public static function getCollectionId(): string; - - abstract protected function enqueueResources( - array $pools, - callable $getConsoleDB - ); + abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void; public function __construct() { - $this->connections = new Connections(); $type = static::getSupportedResource(); $this ->desc("Execute {$type}s scheduled in Appwrite") ->inject('pools') - ->inject('getConsoleDB') + ->inject('dbForConsole') ->inject('getProjectDB') - ->callback(fn (array $pools, callable $getConsoleDB, callable $getProjectDB) => $this->action($pools, $getConsoleDB, $getProjectDB)); + ->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB)); } /** @@ -48,12 +44,11 @@ abstract class ScheduleBase extends Action * 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute * 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker. */ - public function action(array $pools, callable $getConsoleDB, callable $getProjectDB): void + public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void { Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1'); Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started'); - [$_, $_, $dbForConsole] = $getConsoleDB(); /** * Extract only necessary attributes to lower memory used. * @@ -64,12 +59,6 @@ abstract class ScheduleBase extends Action $getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array { $project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId')); - $collectionId = match ($schedule->getAttribute('resourceType')) { - 'function' => 'functions', - 'message' => 'messages', - 'execution' => 'executions' - }; - $resource = $getProjectDB($project)->getDocument( static::getCollectionId(), $schedule->getAttribute('resourceId') @@ -124,73 +113,76 @@ abstract class ScheduleBase extends Action $latestDocument = \end($results); } + $pools->reclaim(); Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds"); Console::success("Starting timers at " . DateTime::now()); - Timer::tick(static::UPDATE_TIMER * 1000, function () use ($getConsoleDB, &$lastSyncUpdate, $getSchedule, $pools) { - [$connection,$pool, $dbForConsole] = $getConsoleDB(); - $connections = new Connections(); - $connections->add($connection, $pool); + run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) { + /** + * The timer synchronize $schedules copy with database collection. + */ + Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) { + $time = DateTime::now(); + $timerStart = \microtime(true); - $time = DateTime::now(); - $timerStart = \microtime(true); + $limit = 1000; + $sum = $limit; + $total = 0; + $latestDocument = null; - $limit = 1000; - $sum = $limit; - $total = 0; - $latestDocument = null; + Console::log("Sync tick: Running at $time"); - Console::log("Sync tick: Running at $time"); + while ($sum === $limit) { + $paginationQueries = [Query::limit($limit)]; - while ($sum === $limit) { - $paginationQueries = [Query::limit($limit)]; - - if ($latestDocument) { - $paginationQueries[] = Query::cursorAfter($latestDocument); - } - - $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ - Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), - Query::equal('resourceType', [static::getSupportedResource()]), - Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), - ])); - - $sum = count($results); - $total = $total + $sum; - - foreach ($results as $document) { - $localDocument = $this->schedules[$document->getInternalId()] ?? null; - - // Check if resource has been updated since last sync - $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; - $new = \strtotime($document['resourceUpdatedAt']); - - if (!$document['active']) { - Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}"); - unset($this->schedules[$document->getInternalId()]); - } elseif ($new !== $org) { - Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}"); - $this->schedules[$document->getInternalId()] = $getSchedule($document); + if ($latestDocument) { + $paginationQueries[] = Query::cursorAfter($latestDocument); } + + $results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [ + Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + Query::equal('resourceType', [static::getSupportedResource()]), + Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate), + ])); + + $sum = count($results); + $total = $total + $sum; + + foreach ($results as $document) { + $localDocument = $this->schedules[$document->getInternalId()] ?? null; + + // Check if resource has been updated since last sync + $org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null; + $new = \strtotime($document['resourceUpdatedAt']); + + if (!$document['active']) { + Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}"); + unset($this->schedules[$document->getInternalId()]); + } elseif ($new !== $org) { + Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}"); + $this->schedules[$document->getInternalId()] = $getSchedule($document); + } + } + + $latestDocument = \end($results); } - $latestDocument = \end($results); - } + $lastSyncUpdate = $time; + $timerEnd = \microtime(true); - $lastSyncUpdate = $time; - $timerEnd = \microtime(true); + $pools->reclaim(); - $connections->reclaim(); - Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); + Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds"); + }); + + Timer::tick( + static::ENQUEUE_TIMER * 1000, + fn () => $this->enqueueResources($pools, $dbForConsole, $getProjectDB) + ); + + $this->enqueueResources($pools, $dbForConsole, $getProjectDB); }); - - Timer::tick( - static::ENQUEUE_TIMER * 1000, - fn () => $this->enqueueResources($pools, $getConsoleDB) - ); - - $this->enqueueResources($pools, $getConsoleDB); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 42bc98a406..73a2814397 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -4,7 +4,8 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Swoole\Coroutine as Co; -use Utopia\Queue\Connection\Redis; +use Utopia\Database\Database; +use Utopia\Pools\Group; class ScheduleExecutions extends ScheduleBase { @@ -26,16 +27,11 @@ class ScheduleExecutions extends ScheduleBase return 'executions'; } - protected function enqueueResources(array $pools, callable $getConsoleDB): void + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { - [$connection,$pool, $dbForConsole] = $getConsoleDB(); - $this->connections->add($connection, $pool); - - $queuePool = $pools['pools-queue-queue']['pool']; - $queueConnection = $queuePool->get(); - $this->connections->add($queueConnection, $queuePool); - - $queueForFunctions = new Func(new Redis($queueConnection)); + $queue = $pools->get('queue')->pop(); + $connection = $queue->getResource(); + $queueForFunctions = new Func($connection); $intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds'); foreach ($this->schedules as $schedule) { @@ -61,7 +57,7 @@ class ScheduleExecutions extends ScheduleBase $delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp(); - \go(function () use ($queueForFunctions, $schedule, $delay, $data, $dbForConsole) { + \go(function () use ($queueForFunctions, $schedule, $delay, $data) { Co::sleep($delay); $queueForFunctions->setType('schedule') @@ -76,16 +72,16 @@ class ScheduleExecutions extends ScheduleBase ->setProject($schedule['project']) ->setUserId($data['userId'] ?? '') ->trigger(); - - $dbForConsole->deleteDocument( - 'schedules', - $schedule['$id'], - ); }); + $dbForConsole->deleteDocument( + 'schedules', + $schedule['$id'], + ); + unset($this->schedules[$schedule['$internalId']]); } - $this->connections->reclaim(); + $queue->reclaim(); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index a035f5ab41..4d57902330 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -5,8 +5,9 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; use Cron\CronExpression; use Utopia\CLI\Console; +use Utopia\Database\Database; use Utopia\Database\DateTime; -use Utopia\Queue\Connection\Redis; +use Utopia\Pools\Group; class ScheduleFunctions extends ScheduleBase { @@ -30,7 +31,7 @@ class ScheduleFunctions extends ScheduleBase return 'functions'; } - protected function enqueueResources(array $pools, callable $getConsoleDB): void + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { $timerStart = \microtime(true); $time = DateTime::now(); @@ -72,11 +73,8 @@ class ScheduleFunctions extends ScheduleBase \go(function () use ($delay, $scheduleKeys, $pools) { \sleep($delay); // in seconds - $pool = $pools['pools-queue-queue']['pool']; - $connection = $pool->get(); - $this->connections->add($connection, $pool); - - $queueConnection = new Redis($connection); + $queue = $pools->get('queue')->pop(); + $connection = $queue->getResource(); foreach ($scheduleKeys as $scheduleKey) { // Ensure schedule was not deleted @@ -86,7 +84,7 @@ class ScheduleFunctions extends ScheduleBase $schedule = $this->schedules[$scheduleKey]; - $queueForFunctions = new Func($queueConnection); + $queueForFunctions = new Func($connection); $queueForFunctions ->setType('schedule') @@ -97,8 +95,7 @@ class ScheduleFunctions extends ScheduleBase ->trigger(); } - $this->connections->reclaim(); - // $queue->reclaim(); // TODO: Do in try/catch/finally, or add to connectons resource + $queue->reclaim(); }); } diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 386c10582f..b9d8e2a282 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -3,7 +3,8 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Messaging; -use Utopia\Queue\Connection\Redis; +use Utopia\Database\Database; +use Utopia\Pools\Group; class ScheduleMessages extends ScheduleBase { @@ -25,11 +26,8 @@ class ScheduleMessages extends ScheduleBase return 'messages'; } - protected function enqueueResources(array $pools, callable $getConsoleDB): void + protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void { - [$connection,$pool, $dbForConsole] = $getConsoleDB(); - $this->connections->add($connection, $pool); - foreach ($this->schedules as $schedule) { if (!$schedule['active']) { continue; @@ -42,15 +40,10 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($now, $schedule, $pools, $dbForConsole) { - $pool = $pools['pools-queue-queue']['pool']; - $dsn = $pools['pools-queue-queue']['dsn']; - $connection = $pool->get(); - $this->connections->add($connection, $pool); - - $queueConnection = new Redis($dsn->getHost(), $dsn->getPort()); - - $queueForMessaging = new Messaging($queueConnection); + \go(function () use ($schedule, $pools, $dbForConsole) { + $queue = $pools->get('queue')->pop(); + $connection = $queue->getResource(); + $queueForMessaging = new Messaging($connection); $queueForMessaging ->setType(MESSAGE_SEND_TYPE_EXTERNAL) @@ -63,7 +56,8 @@ class ScheduleMessages extends ScheduleBase $schedule['$id'], ); - $this->connections->reclaim(); + $queue->reclaim(); + unset($this->schedules[$schedule['$internalId']]); }); } diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 776410e4e4..f71de98d95 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -5,27 +5,25 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Specification\Format\OpenAPI3; use Appwrite\Specification\Format\Swagger2; use Appwrite\Specification\Specification; -use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Models; +use Appwrite\Utopia\Request as AppwriteRequest; +use Appwrite\Utopia\Response as AppwriteResponse; use Exception; -use Swoole\Http\Request as SwooleHttpRequest; -use Swoole\Http\Response as SwooleHttpResponse; +use Swoole\Http\Request as SwooleRequest; +use Swoole\Http\Response as SwooleResponse; +use Utopia\App; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; -use Utopia\DI\Container; -use Utopia\DI\Dependency; -use Utopia\Http\Adapter\FPM\Server; -use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Http\Adapter\Swoole\Response as HttpResponse; -use Utopia\Http\Http; -use Utopia\Http\Validator\Text; -use Utopia\Http\Validator\WhiteList; use Utopia\Platform\Action; +use Utopia\Registry\Registry; +use Utopia\Request as UtopiaRequest; +use Utopia\Response as UtopiaResponse; use Utopia\System\System; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; class Specs extends Action { @@ -34,38 +32,37 @@ class Specs extends Action return 'specs'; } + public function getRequest(): UtopiaRequest + { + return new AppwriteRequest(new SwooleRequest()); + } + + public function getResponse(): UtopiaResponse + { + return new AppwriteResponse(new SwooleResponse()); + } + public function __construct() { $this ->desc('Generate Appwrite API specifications') ->param('version', 'latest', new Text(16), 'Spec version', true) ->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true) - ->inject('context') - ->callback(fn (string $version, string $mode, Container $context) => $this->action($version, $mode, $context)); + ->inject('register') + ->callback(fn (string $version, string $mode, Registry $register) => $this->action($version, $mode, $register)); } - public function action(string $version, string $mode, Container $container): void + public function action(string $version, string $mode, Registry $register): void { - $appRoutes = Http::getRoutes(); - $response = new Response(new HttpResponse(new SwooleHttpResponse())); + $appRoutes = App::getRoutes(); + $response = $this->getResponse(); $mocks = ($mode === 'mocks'); - $requestDependency = new Dependency(); - $responseDependency = new Dependency(); - $dbForConsole = new Dependency(); - $dbForProject = new Dependency(); - // Mock dependencies - $requestDependency->setName('request')->setCallback(fn () => new Request(new SwooleHttpRequest())); - $responseDependency->setName('response')->setCallback(fn () => $response); - $dbForConsole->setName('dbForConsole')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); - $dbForProject->setName('dbForProject')->setCallback(fn () => new Database(new MySQL(''), new Cache(new None()))); - - $container - ->set($requestDependency) - ->set($responseDependency) - ->set($dbForProject) - ->set($dbForConsole); + App::setResource('request', fn () => $this->getRequest()); + App::setResource('response', fn () => $response); + App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None()))); + App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None()))); $platforms = [ 'client' => APP_PLATFORM_CLIENT, @@ -255,7 +252,7 @@ class Specs extends Action ]; } - $models = Models::getModels(); + $models = $response->getModels(); foreach ($models as $key => $value) { if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { @@ -263,7 +260,7 @@ class Specs extends Action } } - $arguments = [new Http(new Server(), $container, 'UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; + $arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; foreach (['swagger2', 'open-api3'] as $format) { $formatInstance = match ($format) { 'swagger2' => new Swagger2(...$arguments), diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 608b924c06..341ce42fc4 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -3,8 +3,8 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Boolean; +use Utopia\Validator\Text; class Upgrade extends Install { diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index d21f1c267d..86ca59d3fd 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -9,7 +9,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Structure; -use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\Platform\Action; use Utopia\Queue\Message; @@ -29,8 +28,7 @@ class Audits extends Action ->desc('Audits worker') ->inject('message') ->inject('dbForProject') - ->inject('authorization') - ->callback(fn ($message, $dbForProject, ValidatorAuthorization $authorization) => $this->action($message, $dbForProject, $authorization)); + ->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject)); } @@ -43,7 +41,7 @@ class Audits extends Action * @throws Authorization * @throws Structure */ - public function action(Message $message, Database $dbForProject, ValidatorAuthorization $auth): void + public function action(Message $message, Database $dbForProject): void { $payload = $message->getPayload() ?? []; diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 9274e7430c..5dd2f7f886 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -54,8 +54,7 @@ class Builds extends Action ->inject('dbForProject') ->inject('deviceForFunctions') ->inject('log') - ->inject('authorization') - ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $authorization) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log, $authorization)); + ->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); } /** @@ -68,11 +67,10 @@ class Builds extends Action * @param Database $dbForProject * @param Device $deviceForFunctions * @param Log $log - * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log, Authorization $auth): void + public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void { $payload = $message->getPayload() ?? []; @@ -94,7 +92,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log, $auth); + $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log); break; default: @@ -115,12 +113,11 @@ class Builds extends Action * @param Document $deployment * @param Document $template * @param Log $log - * @param Authorization $auth * @return void * @throws \Utopia\Database\Exception * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log, Authorization $auth): void + protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void { $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); @@ -224,18 +221,20 @@ class Builds extends Action $templateRootDirectory = \ltrim($templateRootDirectory, '/'); if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { - $output = ''; + $stdout = ''; + $stderr = ''; + // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $output); + $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $output); + throw new \Exception('Unable to clone code repository: ' . $stderr); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); $tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz'; @@ -246,7 +245,7 @@ class Builds extends Action } $tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory)); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -255,7 +254,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $output); + Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr); $directorySize = $deviceForFunctions->getFileSize($source); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -277,7 +276,6 @@ class Builds extends Action $branchName = $deployment->getAttribute('providerBranch'); $commitHash = $deployment->getAttribute('providerCommitHash', ''); - $output = ''; $cloneVersion = $branchName; $cloneType = GitHub::CLONE_TYPE_BRANCH; @@ -285,18 +283,22 @@ class Builds extends Action $cloneVersion = $commitHash; $cloneType = GitHub::CLONE_TYPE_COMMIT; } + $gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory); - Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $output); + $stdout = ''; + $stderr = ''; + + Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr); if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); return; } - $exit = Console::execute($gitCloneCommand, '', $output); + $exit = Console::execute($gitCloneCommand, '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $output); + throw new \Exception('Unable to clone code repository: ' . $stderr); } // Local refactoring for function folder with spaces @@ -304,10 +306,10 @@ class Builds extends Action $rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory); $from = $tmpDirectory . '/' . $rootDirectory; $to = $tmpDirectory . '/' . $rootDirectoryWithoutSpaces; - $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $output); + $exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to move function with spaces' . $output); + throw new \Exception('Unable to move function with spaces' . $stderr); } $rootDirectory = $rootDirectoryWithoutSpaces; } @@ -328,33 +330,33 @@ class Builds extends Action $tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template'; $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - $exit = Console::execute($gitCloneCommandForTemplate, '', $output); + $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to clone code repository: ' . $output); + throw new \Exception('Unable to clone code repository: ' . $stderr); } // Ensure directories - Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $output); - Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); + Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr); + Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); // Merge template into user repo - Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $output); + Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr); // Commit and push - $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $output); + $exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to push code repository: ' . $output); + throw new \Exception('Unable to push code repository: ' . $stderr); } - $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $output); + $exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr); if ($exit !== 0) { - throw new \Exception('Unable to get vcs commit SHA: ' . $output); + throw new \Exception('Unable to get vcs commit SHA: ' . $stderr); } - $providerCommitHash = \trim($output); + $providerCommitHash = \trim($stdout); $authorUrl = "https://github.com/$cloneOwner"; $deployment->setAttribute('providerCommitHash', $providerCommitHash ?? ''); @@ -397,7 +399,7 @@ class Builds extends Action } $tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory); - Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $output); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax + Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); @@ -406,7 +408,7 @@ class Builds extends Action throw new \Exception("Unable to move file"); } - Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $output); + Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); @@ -661,7 +663,7 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - $auth->skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); } catch (\Throwable $th) { if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 07e1dcdaa6..58dc1dd28a 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -11,6 +11,7 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model\Rule; use Exception; use Throwable; +use Utopia\App; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -21,7 +22,6 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Domains\Domain; -use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -126,7 +126,7 @@ class Certificates extends Action $certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]); // If we don't have certificate for domain yet, let's create new document. At the end we save it - if ($certificate->isEmpty()) { + if (!$certificate) { $certificate = new Document(); $certificate->setAttribute('domain', $domain->get()); } @@ -216,7 +216,7 @@ class Certificates extends Action { // Check if update or insert required $certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]); - if (!$certificateDocument->isEmpty()) { + if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { // Merge new data with current data $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); @@ -330,26 +330,30 @@ class Certificates extends Action * * @param string $folder Folder into which certificates should be generated * @param string $domain Domain to generate certificate for - * @return string output + * @return array Named array with keys 'stdout' and 'stderr', both string * @throws Exception */ - private function issueCertificate(string $folder, string $domain, string $email): string + private function issueCertificate(string $folder, string $domain, string $email): array { - $output = ''; + $stdout = ''; + $stderr = ''; - $staging = (Http::isProduction()) ? '' : ' --dry-run'; + $staging = (App::isProduction()) ? '' : ' --dry-run'; $exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}" . " --email " . $email . " --cert-name " . $folder . " -w " . APP_STORAGE_CERTIFICATES - . " -d {$domain}", '', $output); + . " -d {$domain}", '', $stdout, $stderr); // Unexpected error, usually 5XX, API limits, ... if ($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: ' . $output); + throw new Exception('Failed to issue a certificate with message: ' . $stderr); } - return $output; + return [ + 'stdout' => $stdout, + 'stderr' => $stderr + ]; } /** @@ -377,7 +381,7 @@ class Certificates extends Action * @return void * @throws Exception */ - private function applyCertificateFiles(string $folder, string $domain, string $letsEncryptData): void + private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void { // Prepare folder in storage for domain @@ -390,19 +394,19 @@ class Certificates extends Action // Move generated files if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData); + throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData); + throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData); + throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); } if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData); + throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); } $config = \implode(PHP_EOL, [ @@ -478,7 +482,7 @@ class Certificates extends Action Query::equal('domain', [$domain]), ]); - if (!$rule->isEmpty()) { + if ($rule !== false && !$rule->isEmpty()) { $rule->setAttribute('certificateId', $certificateId); $rule->setAttribute('status', $success ? 'verified' : 'unverified'); $dbForConsole->updateDocument('rules', $rule->getId(), $rule); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 53a839254d..62fd8cd177 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -22,7 +22,6 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -55,15 +54,14 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->inject('authorization') - ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $authorization) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log, $authorization)); + ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log)); } /** * @throws Exception * @throws Throwable */ - public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log, ValidatorAuthorization $auth): void + public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void { $payload = $message->getPayload() ?? []; @@ -119,7 +117,7 @@ class Deletes extends Action break; case DELETE_TYPE_AUDIT: if (!$project->isEmpty()) { - $this->deleteAuditLogs($project, $getProjectDB, $auditRetention, $auth); + $this->deleteAuditLogs($project, $getProjectDB, $auditRetention); } if (!$document->isEmpty()) { @@ -127,7 +125,7 @@ class Deletes extends Action } break; case DELETE_TYPE_ABUSE: - $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention, $auth); + $this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention); break; case DELETE_TYPE_REALTIME: $this->deleteRealtimeUsage($dbForConsole, $datetime); @@ -701,7 +699,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention, ValidatorAuthorization $auth): void + private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -722,7 +720,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention, ValidatorAuthorization $auth): void + private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 51bdb56cf0..e48f96ea90 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -84,13 +84,13 @@ class Mails extends Action $bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'; } $bodyTemplate = Template::fromFile($bodyTemplate); - $bodyTemplate->setParam('{{body}}', $body, escape: false); + $bodyTemplate->setParam('{{body}}', $body, escapeHtml: false); foreach ($variables as $key => $value) { // TODO: hotfix for redirect param - $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: $key !== 'redirect'); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect'); } foreach ($this->richTextParams as $key => $value) { - $bodyTemplate->setParam('{{' . $key . '}}', $value, escape: false); + $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false); } $body = $bodyTemplate->render(); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index ddb72803e3..510fec0431 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -178,7 +178,7 @@ class Messaging extends Action Query::equal('type', [$providerType]), ]); - if ($default->isEmpty()) { + if ($default === false || $default->isEmpty()) { $dbForProject->updateDocument('messages', $message->getId(), $message->setAttributes([ 'status' => MessageStatus::FAILED, 'deliveryErrors' => ['No enabled provider found.'] @@ -275,7 +275,7 @@ class Messaging extends Action Query::equal('identifier', [$result['recipient']]) ]); - if (!$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { $dbForProject->updateDocument( 'targets', $target->getId(), diff --git a/src/Appwrite/Promises/Promise.php b/src/Appwrite/Promises/Promise.php index f12590dfed..a6b1aa79d5 100644 --- a/src/Appwrite/Promises/Promise.php +++ b/src/Appwrite/Promises/Promise.php @@ -12,7 +12,7 @@ abstract class Promise private mixed $result; - final public function __construct(?callable $executor = null) + public function __construct(?callable $executor = null) { if (\is_null($executor)) { return; diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php index 8cddd567f3..c258ef6a5e 100644 --- a/src/Appwrite/Promises/Swoole.php +++ b/src/Appwrite/Promises/Swoole.php @@ -6,16 +6,23 @@ use Swoole\Coroutine\Channel; class Swoole extends Promise { + public function __construct(?callable $executor = null) + { + parent::__construct($executor); + } + protected function execute( callable $executor, callable $resolve, callable $reject ): void { - try { - $executor($resolve, $reject); - } catch (\Throwable $exception) { - $reject($exception); - } + \go(function () use ($executor, $resolve, $reject) { + try { + $executor($resolve, $reject); + } catch (\Throwable $exception) { + $reject($exception); + } + }); } /** diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index e2048971be..30ce6470e1 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -3,13 +3,13 @@ namespace Appwrite\Specification; use Appwrite\Utopia\Response\Model; +use Utopia\App; use Utopia\Config\Config; -use Utopia\Http\Http; -use Utopia\Http\Route; +use Utopia\Route; abstract class Format { - protected Http $http; + protected App $app; /** * @var Route[] @@ -50,9 +50,9 @@ abstract class Format ] ]; - public function __construct(Http $http, array $services, array $routes, array $models, array $keys, int $authCount) + public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount) { - $this->http = $http; + $this->app = $app; $this->services = $services; $this->routes = $routes; $this->models = $models; diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index 46a2552fd8..f7430ec70e 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -7,11 +7,11 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Http\Validator; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Nullable; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\WhiteList; +use Utopia\Validator; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Nullable; +use Utopia\Validator\Range; +use Utopia\Validator\WhiteList; class OpenAPI3 extends Format { @@ -238,11 +238,8 @@ class OpenAPI3 extends Format } if ($route->getLabel('sdk.response.code', 500) === 204) { - $labelCode = (string)$route->getLabel('sdk.response.code', '500'); - $temp['responses'][$labelCode]['description'] = 'No content'; - if (isset($temp['responses'][$labelCode]['schema'])) { - unset($temp['responses'][$labelCode]['schema']); - } + $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['description'] = 'No content'; + unset($temp['responses'][(string)$route->getLabel('sdk.response.code', '500')]['schema']); } if ((!empty($scope))) { // && 'public' != $scope @@ -272,10 +269,10 @@ class OpenAPI3 extends Format $bodyRequired = []; foreach ($route->getParams() as $name => $param) { // Set params - $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); - - /** @var Validator $validator */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; + /** + * @var \Utopia\Validator $validator + */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; $node = [ 'name' => $name, @@ -292,11 +289,11 @@ class OpenAPI3 extends Format switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Database\Validator\UID': - case 'Utopia\Http\Validator\Text': + case 'Utopia\Validator\Text': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Http\Validator\Boolean': + case 'Utopia\Validator\Boolean': $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = false; break; @@ -317,15 +314,15 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'email'; $node['schema']['x-example'] = 'email@example.com'; break; - case 'Utopia\Http\Validator\Host': - case 'Utopia\Http\Validator\URL': + case 'Utopia\Validator\Host': + case 'Utopia\Validator\URL': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; break; - case 'Utopia\Http\Validator\JSON': - case 'Utopia\Http\Validator\Mock': - case 'Utopia\Http\Validator\Assoc': + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': $param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['schema']['type'] = 'object'; $node['schema']['x-example'] = '{}'; @@ -335,7 +332,7 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'binary'; break; - case 'Utopia\Http\Validator\ArrayList': + case 'Utopia\Validator\ArrayList': /** @var ArrayList $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -397,25 +394,25 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'phone'; $node['schema']['x-example'] = '+12065550100'; // In the US, 555 is reserved like example.com break; - case 'Utopia\Http\Validator\Range': + case 'Utopia\Validator\Range': /** @var Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = $validator->getMin(); break; - case 'Utopia\Http\Validator\Numeric': - case 'Utopia\Http\Validator\Integer': + case 'Utopia\Validator\Numeric': + case 'Utopia\Validator\Integer': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'int32'; break; - case 'Utopia\Http\Validator\FloatValidator': + case 'Utopia\Validator\FloatValidator': $node['schema']['type'] = 'number'; $node['schema']['format'] = 'float'; break; - case 'Utopia\Http\Validator\Length': + case 'Utopia\Validator\Length': $node['schema']['type'] = $validator->getType(); break; - case 'Utopia\Http\Validator\WhiteList': + case 'Utopia\Validator\WhiteList': /** @var WhiteList $validator */ $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index a12e9a4ed6..f2e324c71f 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -7,10 +7,10 @@ use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Http\Validator; -use Utopia\Http\Validator\ArrayList; -use Utopia\Http\Validator\Nullable; -use Utopia\Http\Validator\Range; +use Utopia\Validator; +use Utopia\Validator\ArrayList; +use Utopia\Validator\Nullable; +use Utopia\Validator\Range; class Swagger2 extends Format { @@ -270,10 +270,8 @@ class Swagger2 extends Format ); foreach ($parameters as $name => $param) { // Set params - $injections = array_map(fn ($injection) => $this->http->getContainer()->get($injection), $param['injections'] ?? []); - /** @var Validator $validator */ - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $injections) : $param['validator']; + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; $node = [ 'name' => $name, @@ -308,12 +306,12 @@ class Swagger2 extends Format } switch ($class) { - case 'Utopia\Http\Validator\Text': + case 'Utopia\Validator\Text': case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Http\Validator\Boolean': + case 'Utopia\Validator\Boolean': $node['type'] = $validator->getType(); $node['x-example'] = false; break; @@ -334,13 +332,13 @@ class Swagger2 extends Format $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; break; - case 'Utopia\Http\Validator\Host': - case 'Utopia\Http\Validator\URL': + case 'Utopia\Validator\Host': + case 'Utopia\Validator\URL': $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; - case 'Utopia\Http\Validator\ArrayList': + case 'Utopia\Validator\ArrayList': /** @var ArrayList $validator */ $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; @@ -348,9 +346,9 @@ class Swagger2 extends Format 'type' => $validator->getValidator()->getType(), ]; break; - case 'Utopia\Http\Validator\JSON': - case 'Utopia\Http\Validator\Mock': - case 'Utopia\Http\Validator\Assoc': + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = '{}'; @@ -399,26 +397,26 @@ class Swagger2 extends Format $node['format'] = 'phone'; $node['x-example'] = '+12065550100'; break; - case 'Utopia\Http\Validator\Range': + case 'Utopia\Validator\Range': /** @var Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = $validator->getMin(); break; - case 'Utopia\Http\Validator\Numeric': - case 'Utopia\Http\Validator\Integer': + case 'Utopia\Validator\Numeric': + case 'Utopia\Validator\Integer': $node['type'] = $validator->getType(); $node['format'] = 'int32'; break; - case 'Utopia\Http\Validator\FloatValidator': + case 'Utopia\Validator\FloatValidator': $node['type'] = 'number'; $node['format'] = 'float'; break; - case 'Utopia\Http\Validator\Length': + case 'Utopia\Validator\Length': $node['type'] = $validator->getType(); break; - case 'Utopia\Http\Validator\WhiteList': - /** @var \Utopia\Http\Validator\WhiteList $validator */ + case 'Utopia\Validator\WhiteList': + /** @var \Utopia\Validator\WhiteList $validator */ $node['type'] = $validator->getType(); $node['x-example'] = $validator->getList()[0]; diff --git a/src/Appwrite/Task/Validator/Cron.php b/src/Appwrite/Task/Validator/Cron.php index afa19c50a6..03bd1c5220 100644 --- a/src/Appwrite/Task/Validator/Cron.php +++ b/src/Appwrite/Task/Validator/Cron.php @@ -3,7 +3,7 @@ namespace Appwrite\Task\Validator; use Cron\CronExpression; -use Utopia\Http\Validator; +use Utopia\Validator; class Cron extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php index b851d8ba85..3f23500952 100644 --- a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php +++ b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php @@ -3,7 +3,7 @@ namespace Appwrite\Utopia\Database\Validator; use Utopia\Database\Validator\UID; -use Utopia\Http\Validator; +use Utopia\Validator; class CompoundUID extends Validator { diff --git a/src/Appwrite/Utopia/Database/Validator/ProjectId.php b/src/Appwrite/Utopia/Database/Validator/ProjectId.php index 28abe176fe..46b0cdf53e 100644 --- a/src/Appwrite/Utopia/Database/Validator/ProjectId.php +++ b/src/Appwrite/Utopia/Database/Validator/ProjectId.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia\Database\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; class ProjectId extends Validator { diff --git a/src/Appwrite/Utopia/Queue/Connections.php b/src/Appwrite/Utopia/Queue/Connections.php deleted file mode 100644 index e873373566..0000000000 --- a/src/Appwrite/Utopia/Queue/Connections.php +++ /dev/null @@ -1,36 +0,0 @@ -connections[] = ['connection' => $connection, 'pool' => $pool]; - return $this; - } - - /** - * @return self - */ - public function reclaim(): self - { - foreach ($this->connections as $id => $resource) { - $pool = $resource['pool']; - $connection = $resource['connection']; - $pool->put($connection); - unset($this->connections[$id]); - } - - return $this; - } -} diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 939a95b022..26c1baf188 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -3,10 +3,11 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Request\Filter; -use Utopia\Http\Adapter\Swoole\Request as HttpRequest; -use Utopia\Http\Route; +use Swoole\Http\Request as SwooleRequest; +use Utopia\Route; +use Utopia\Swoole\Request as UtopiaRequest; -class Request extends HttpRequest +class Request extends UtopiaRequest { /** * @var array @@ -14,12 +15,9 @@ class Request extends HttpRequest private array $filters = []; private static ?Route $route = null; - /** - * Request constructor. - */ - public function __construct(HttpRequest $request) + public function __construct(SwooleRequest $request) { - parent::__construct($request->swoole); + parent::__construct($request); } /** @@ -115,16 +113,6 @@ class Request extends HttpRequest return self::$route !== null; } - - public function removeHeader(string $key): static - { - if (isset($this->headers[$key])) { - unset($this->headers[$key]); - } - - return parent::removeHeader($key); - } - /** * Get headers * @@ -134,18 +122,14 @@ class Request extends HttpRequest */ public function getHeaders(): array { - if ($this->headers !== null) { - return $this->headers; - } - try { - $this->headers = $this->generateHeaders(); + $headers = $this->generateHeaders(); } catch (\Throwable) { - $this->headers = []; + $headers = []; } if (empty($this->swoole->cookie)) { - return $this->headers; + return $headers; } $cookieHeaders = []; @@ -154,10 +138,10 @@ class Request extends HttpRequest } if (!empty($cookieHeaders)) { - $this->headers['cookie'] = \implode('; ', $cookieHeaders); + $headers['cookie'] = \implode('; ', $cookieHeaders); } - return $this->headers; + return $headers; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 376c327b37..6cc2639f51 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -4,20 +4,122 @@ namespace Appwrite\Utopia; use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; -use Appwrite\Utopia\Response\Models; +use Appwrite\Utopia\Response\Model; +use Appwrite\Utopia\Response\Model\Account; +use Appwrite\Utopia\Response\Model\AlgoArgon2; +use Appwrite\Utopia\Response\Model\AlgoBcrypt; +use Appwrite\Utopia\Response\Model\AlgoMd5; +use Appwrite\Utopia\Response\Model\AlgoPhpass; +use Appwrite\Utopia\Response\Model\AlgoScrypt; +use Appwrite\Utopia\Response\Model\AlgoScryptModified; +use Appwrite\Utopia\Response\Model\AlgoSha; +use Appwrite\Utopia\Response\Model\Any; +use Appwrite\Utopia\Response\Model\Attribute; +use Appwrite\Utopia\Response\Model\AttributeBoolean; +use Appwrite\Utopia\Response\Model\AttributeDatetime; +use Appwrite\Utopia\Response\Model\AttributeEmail; +use Appwrite\Utopia\Response\Model\AttributeEnum; +use Appwrite\Utopia\Response\Model\AttributeFloat; +use Appwrite\Utopia\Response\Model\AttributeInteger; +use Appwrite\Utopia\Response\Model\AttributeIP; +use Appwrite\Utopia\Response\Model\AttributeList; +use Appwrite\Utopia\Response\Model\AttributeRelationship; +use Appwrite\Utopia\Response\Model\AttributeString; +use Appwrite\Utopia\Response\Model\AttributeURL; +use Appwrite\Utopia\Response\Model\AuthProvider; +use Appwrite\Utopia\Response\Model\BaseList; +use Appwrite\Utopia\Response\Model\Branch; +use Appwrite\Utopia\Response\Model\Bucket; +use Appwrite\Utopia\Response\Model\Build; +use Appwrite\Utopia\Response\Model\Collection; +use Appwrite\Utopia\Response\Model\ConsoleVariables; +use Appwrite\Utopia\Response\Model\Continent; +use Appwrite\Utopia\Response\Model\Country; +use Appwrite\Utopia\Response\Model\Currency; +use Appwrite\Utopia\Response\Model\Database; +use Appwrite\Utopia\Response\Model\Deployment; +use Appwrite\Utopia\Response\Model\Detection; +use Appwrite\Utopia\Response\Model\Document as ModelDocument; +use Appwrite\Utopia\Response\Model\Error; +use Appwrite\Utopia\Response\Model\ErrorDev; +use Appwrite\Utopia\Response\Model\Execution; +use Appwrite\Utopia\Response\Model\File; +use Appwrite\Utopia\Response\Model\Func; +use Appwrite\Utopia\Response\Model\Headers; +use Appwrite\Utopia\Response\Model\HealthAntivirus; +use Appwrite\Utopia\Response\Model\HealthCertificate; +use Appwrite\Utopia\Response\Model\HealthQueue; +use Appwrite\Utopia\Response\Model\HealthStatus; +use Appwrite\Utopia\Response\Model\HealthTime; +use Appwrite\Utopia\Response\Model\HealthVersion; +use Appwrite\Utopia\Response\Model\Identity; +use Appwrite\Utopia\Response\Model\Index; +use Appwrite\Utopia\Response\Model\Installation; +use Appwrite\Utopia\Response\Model\JWT; +use Appwrite\Utopia\Response\Model\Key; +use Appwrite\Utopia\Response\Model\Language; +use Appwrite\Utopia\Response\Model\Locale; +use Appwrite\Utopia\Response\Model\LocaleCode; +use Appwrite\Utopia\Response\Model\Log; +use Appwrite\Utopia\Response\Model\Membership; +use Appwrite\Utopia\Response\Model\Message; +use Appwrite\Utopia\Response\Model\Metric; +use Appwrite\Utopia\Response\Model\MetricBreakdown; +use Appwrite\Utopia\Response\Model\MFAChallenge; +use Appwrite\Utopia\Response\Model\MFAFactors; +use Appwrite\Utopia\Response\Model\MFARecoveryCodes; +use Appwrite\Utopia\Response\Model\MFAType; +use Appwrite\Utopia\Response\Model\Migration; +use Appwrite\Utopia\Response\Model\MigrationFirebaseProject; +use Appwrite\Utopia\Response\Model\MigrationReport; +use Appwrite\Utopia\Response\Model\Mock; +use Appwrite\Utopia\Response\Model\MockNumber; +use Appwrite\Utopia\Response\Model\None; +use Appwrite\Utopia\Response\Model\Phone; +use Appwrite\Utopia\Response\Model\Platform; +use Appwrite\Utopia\Response\Model\Preferences; +use Appwrite\Utopia\Response\Model\Project; +use Appwrite\Utopia\Response\Model\Provider; +use Appwrite\Utopia\Response\Model\ProviderRepository; +use Appwrite\Utopia\Response\Model\Rule; +use Appwrite\Utopia\Response\Model\Runtime; +use Appwrite\Utopia\Response\Model\Session; +use Appwrite\Utopia\Response\Model\Specification; +use Appwrite\Utopia\Response\Model\Subscriber; +use Appwrite\Utopia\Response\Model\Target; +use Appwrite\Utopia\Response\Model\Team; +use Appwrite\Utopia\Response\Model\TemplateEmail; +use Appwrite\Utopia\Response\Model\TemplateFunction; +use Appwrite\Utopia\Response\Model\TemplateRuntime; +use Appwrite\Utopia\Response\Model\TemplateSMS; +use Appwrite\Utopia\Response\Model\TemplateVariable; +use Appwrite\Utopia\Response\Model\Token; +use Appwrite\Utopia\Response\Model\Topic; +use Appwrite\Utopia\Response\Model\UsageBuckets; +use Appwrite\Utopia\Response\Model\UsageCollection; +use Appwrite\Utopia\Response\Model\UsageDatabase; +use Appwrite\Utopia\Response\Model\UsageDatabases; +use Appwrite\Utopia\Response\Model\UsageFunction; +use Appwrite\Utopia\Response\Model\UsageFunctions; +use Appwrite\Utopia\Response\Model\UsageProject; +use Appwrite\Utopia\Response\Model\UsageStorage; +use Appwrite\Utopia\Response\Model\UsageUsers; +use Appwrite\Utopia\Response\Model\User; +use Appwrite\Utopia\Response\Model\Variable; +use Appwrite\Utopia\Response\Model\VcsContent; +use Appwrite\Utopia\Response\Model\Webhook; use Exception; use JsonException; +use Swoole\Http\Response as SwooleHTTPResponse; // Keep last use Utopia\Database\Document; -use Utopia\Http\Adapter\Swoole\Response as HttpResponse; - -// Keep last +use Utopia\Swoole\Response as SwooleResponse; /** * @method int getStatusCode() * @method Response setStatusCode(int $code = 200) */ -class Response extends HttpResponse +class Response extends SwooleResponse { // General public const MODEL_NONE = 'none'; @@ -228,9 +330,162 @@ class Response extends HttpResponse * * @param float $time */ - public function __construct(HttpResponse $response) + public function __construct(SwooleHTTPResponse $response) { - parent::__construct($response->swoole); + $this + // General + ->setModel(new None()) + ->setModel(new Any()) + ->setModel(new Error()) + ->setModel(new ErrorDev()) + // Lists + ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) + ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) + ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) + ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) + ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) + ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) + ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) + ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) + ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) + ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) + ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) + ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) + ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) + ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) + ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) + ->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY)) + ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) + ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) + ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) + ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) + ->setModel(new BaseList('Builds List', self::MODEL_BUILD_LIST, 'builds', self::MODEL_BUILD)) // Not used anywhere yet + ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) + ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) + ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) + ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) + ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) + ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) + ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) + ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) + ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) + ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) + ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) + ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) + ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) + ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) + ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) + ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) + ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) + ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) + ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) + ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) + ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) + ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) + ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) + ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) + // Entities + ->setModel(new Database()) + ->setModel(new Collection()) + ->setModel(new Attribute()) + ->setModel(new AttributeList()) + ->setModel(new AttributeString()) + ->setModel(new AttributeInteger()) + ->setModel(new AttributeFloat()) + ->setModel(new AttributeBoolean()) + ->setModel(new AttributeEmail()) + ->setModel(new AttributeEnum()) + ->setModel(new AttributeIP()) + ->setModel(new AttributeURL()) + ->setModel(new AttributeDatetime()) + ->setModel(new AttributeRelationship()) + ->setModel(new Index()) + ->setModel(new ModelDocument()) + ->setModel(new Log()) + ->setModel(new User()) + ->setModel(new AlgoMd5()) + ->setModel(new AlgoSha()) + ->setModel(new AlgoPhpass()) + ->setModel(new AlgoBcrypt()) + ->setModel(new AlgoScrypt()) + ->setModel(new AlgoScryptModified()) + ->setModel(new AlgoArgon2()) + ->setModel(new Account()) + ->setModel(new Preferences()) + ->setModel(new Session()) + ->setModel(new Identity()) + ->setModel(new Token()) + ->setModel(new JWT()) + ->setModel(new Locale()) + ->setModel(new LocaleCode()) + ->setModel(new File()) + ->setModel(new Bucket()) + ->setModel(new Team()) + ->setModel(new Membership()) + ->setModel(new Func()) + ->setModel(new TemplateFunction()) + ->setModel(new TemplateRuntime()) + ->setModel(new TemplateVariable()) + ->setModel(new Installation()) + ->setModel(new ProviderRepository()) + ->setModel(new Detection()) + ->setModel(new VcsContent()) + ->setModel(new Branch()) + ->setModel(new Runtime()) + ->setModel(new Deployment()) + ->setModel(new Execution()) + ->setModel(new Build()) + ->setModel(new Project()) + ->setModel(new Webhook()) + ->setModel(new Key()) + ->setModel(new MockNumber()) + ->setModel(new AuthProvider()) + ->setModel(new Platform()) + ->setModel(new Variable()) + ->setModel(new Country()) + ->setModel(new Continent()) + ->setModel(new Language()) + ->setModel(new Currency()) + ->setModel(new Phone()) + ->setModel(new HealthAntivirus()) + ->setModel(new HealthQueue()) + ->setModel(new HealthStatus()) + ->setModel(new HealthCertificate()) + ->setModel(new HealthTime()) + ->setModel(new HealthVersion()) + ->setModel(new Metric()) + ->setModel(new MetricBreakdown()) + ->setModel(new UsageDatabases()) + ->setModel(new UsageDatabase()) + ->setModel(new UsageCollection()) + ->setModel(new UsageUsers()) + ->setModel(new UsageStorage()) + ->setModel(new UsageBuckets()) + ->setModel(new UsageFunctions()) + ->setModel(new UsageFunction()) + ->setModel(new UsageProject()) + ->setModel(new Headers()) + ->setModel(new Specification()) + ->setModel(new Rule()) + ->setModel(new TemplateSMS()) + ->setModel(new TemplateEmail()) + ->setModel(new ConsoleVariables()) + ->setModel(new MFAChallenge()) + ->setModel(new MFARecoveryCodes()) + ->setModel(new MFAType()) + ->setModel(new MFAFactors()) + ->setModel(new Provider()) + ->setModel(new Message()) + ->setModel(new Topic()) + ->setModel(new Subscriber()) + ->setModel(new Target()) + ->setModel(new Migration()) + ->setModel(new MigrationReport()) + ->setModel(new MigrationFirebaseProject()) + // Tests (keep last) + ->setModel(new Mock()); + + parent::__construct($response); } /** @@ -240,6 +495,49 @@ class Response extends HttpResponse public const CONTENT_TYPE_NULL = 'null'; public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; + /** + * List of defined output objects + */ + protected $models = []; + + /** + * Set Model Object + * + * @return self + */ + public function setModel(Model $instance) + { + $this->models[$instance->getType()] = $instance; + + return $this; + } + + /** + * Get Model Object + * + * @param string $key + * @return Model + * @throws Exception + */ + public function getModel(string $key): Model + { + if (!isset($this->models[$key])) { + throw new Exception('Undefined model: ' . $key); + } + + return $this->models[$key]; + } + + /** + * Get Models List + * + * @return Model[] + */ + public function getModels(): array + { + return $this->models; + } + public function applyFilters(array $data, string $model): array { foreach ($this->filters as $filter) { @@ -307,7 +605,7 @@ class Response extends HttpResponse public function output(Document $document, string $model): array { $data = clone $document; - $model = Models::getModel($model); + $model = $this->getModel($model); $output = []; $data = $model->filter($data); @@ -342,7 +640,7 @@ class Response extends HttpResponse if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { $condition = false; - foreach (Models::getModel($type)->conditions as $attribute => $val) { + foreach ($this->getModel($type)->conditions as $attribute => $val) { $condition = $item->getAttribute($attribute) === $val; if (!$condition) { break; @@ -357,7 +655,7 @@ class Response extends HttpResponse $ruleType = $rule['type']; } - if (!array_key_exists($ruleType, Models::getModels())) { + if (!array_key_exists($ruleType, $this->models)) { throw new Exception('Missing model for rule: ' . $ruleType); } diff --git a/src/Appwrite/Utopia/Response/Models.php b/src/Appwrite/Utopia/Response/Models.php deleted file mode 100644 index 2a0393321c..0000000000 --- a/src/Appwrite/Utopia/Response/Models.php +++ /dev/null @@ -1,304 +0,0 @@ -getType()] = $instance; - } - - /** - * Get Model Object - * - * @param string $key - * @return Model - * @throws Exception - */ - public static function getModel(string $key): Model - { - if (!isset(self::$models[$key])) { - throw new Exception('Undefined model: ' . $key); - } - - return self::$models[$key]; - } - - public static function getModels(): array - { - return self::$models; - } -} diff --git a/src/Appwrite/Utopia/View.php b/src/Appwrite/Utopia/View.php index 60cafb1ca8..e4ed8164c9 100644 --- a/src/Appwrite/Utopia/View.php +++ b/src/Appwrite/Utopia/View.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia; -use Utopia\View\View as OldView; +use Utopia\View as OldView; class View extends OldView { diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 28c3227979..04f2dbd8c8 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -12,7 +12,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Http\Validator\JSON; +use Utopia\Validator\JSON; trait DatabasesBase { @@ -2869,7 +2869,7 @@ trait DatabasesBase $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']); $this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']); $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']); - $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and '.\number_format(PHP_INT_MAX), $tooLow['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']); } /** diff --git a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php index 9b7be063db..8484996058 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php @@ -64,10 +64,9 @@ class DatabasesCustomClientTest extends Scope 'required' => true, ]); - $this->assertEquals(202, $response['headers']['status-code']); - sleep(1); + $this->assertEquals(202, $response['headers']['status-code']); // Document aliases write to update, delete $document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ @@ -83,7 +82,6 @@ class DatabasesCustomClientTest extends Scope ] ]); - $this->assertEquals(201, $document1['headers']['status-code']); $this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']); diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php index 4288b92613..ca8753f374 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php @@ -17,14 +17,6 @@ class DatabasesPermissionsGuestTest extends Scope use SideClient; use DatabasesPermissionsScope; - protected Authorization $auth; - - public function setUp(): void - { - parent::setUp(); - $this->auth = new Authorization(); - } - public function createCollection(): array { $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -119,8 +111,8 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(201, $publicResponse['headers']['status-code']); $this->assertEquals(201, $privateResponse['headers']['status-code']); - $roles = $this->auth->getRoles(); - $this->auth->cleanRoles(); + $roles = Authorization::getRoles(); + Authorization::cleanRoles(); $publicDocuments = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -142,7 +134,7 @@ class DatabasesPermissionsGuestTest extends Scope } foreach ($roles as $role) { - $this->auth->addRole($role); + Authorization::setRole($role); } } @@ -153,8 +145,8 @@ class DatabasesPermissionsGuestTest extends Scope $privateCollectionId = $data['privateCollectionId']; $databaseId = $data['databaseId']; - $roles = $this->auth->getRoles(); - $this->auth->cleanRoles(); + $roles = Authorization::getRoles(); + Authorization::cleanRoles(); $publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [ 'content-type' => 'application/json', @@ -230,7 +222,7 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(401, $privateDocument['headers']['status-code']); foreach ($roles as $role) { - $this->auth->addRole($role); + Authorization::setRole($role); } } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index cd0fcee6ba..a1bb8f2b21 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -11,7 +11,8 @@ trait FunctionsBase { use Async; - protected string $output = ''; + protected string $stdout = ''; + protected string $stderr = ''; protected function setupFunction(mixed $params): string { @@ -145,7 +146,8 @@ trait FunctionsBase { $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; $tarPath = "$folderPath/code.tar.gz"; - Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); if (filesize($tarPath) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f605564726..59a3041a3f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -553,7 +553,7 @@ class FunctionsCustomServerTest extends Scope $folder = 'php-large'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->output); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); $chunkSize = 5 * 1024 * 1024; $handle = @fopen($code, "rb"); @@ -1859,11 +1859,10 @@ class FunctionsCustomServerTest extends Scope 'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('name', ['Test2'])->toString(), - ] + 'queries' => [ 'equal("name", ["Test2"])' ] ] ); + $this->assertEquals(200, $response['headers']['status-code']); $this->assertCount(1, $response['body']['functions']); $this->assertEquals('Test2', $response['body']['functions'][0]['name']); diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index 4f4b0c960d..b1315103b1 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -1,13 +1,12 @@ stdout); + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); if (filesize($tarPath) > 1024 * 1024 * 5) { throw new \Exception('Code package is too large. Use the chunked upload method instead.'); diff --git a/tests/e2e/Services/GraphQL/StorageClientTest.php b/tests/e2e/Services/GraphQL/StorageClientTest.php index 7d6d617c87..9896598c2d 100644 --- a/tests/e2e/Services/GraphQL/StorageClientTest.php +++ b/tests/e2e/Services/GraphQL/StorageClientTest.php @@ -183,6 +183,7 @@ class StorageClientTest extends Scope /** * @depends testCreateFile * @param $file + * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 6be103629e..7fea895b1c 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -232,6 +232,7 @@ class StorageServerTest extends Scope /** * @depends testCreateFile * @param $file + * @return array * @throws \Exception */ public function testGetFileDownload($file) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c4ef8f9d1c..05893be3a8 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1691,7 +1691,7 @@ class ProjectsConsoleClientTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items', $response['body']['message']); + $this->assertEquals('Invalid `numbers` param: Value must a valid array no longer than 10 items and Phone number must start with a \'+\' can have a maximum of fifteen digits.', $response['body']['message']); /** * Test for success diff --git a/tests/e2e/Services/Storage/StorageCustomClientTest.php b/tests/e2e/Services/Storage/StorageCustomClientTest.php index 55340ab849..c723fba50a 100644 --- a/tests/e2e/Services/Storage/StorageCustomClientTest.php +++ b/tests/e2e/Services/Storage/StorageCustomClientTest.php @@ -1089,7 +1089,7 @@ class StorageCustomClientTest extends Scope $this->assertEquals(200, $file['headers']['status-code']); - // Team 2 view success + // Team 1 view success $file = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1112,8 +1112,6 @@ class StorageCustomClientTest extends Scope 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'), ]); - $this->assertEquals($file['headers']['status-code'], 401); - // Team 2 create failure $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', [ 'content-type' => 'multipart/form-data', diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index 54d55e5e68..d2f132e960 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -486,10 +486,11 @@ class WebhooksCustomServerTest extends Scope /** * Test for SUCCESS */ - $output = ''; + $stderr = ''; + $stdout = ''; $folder = 'timeout'; $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $output); + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index 8f86fb8374..a34b4fcf88 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -324,7 +324,7 @@ services: - MYSQL_USER=user - MYSQL_PASSWORD=password - MARIADB_AUTO_UPGRADE=1 - command: 'mysqld --innodb-flush-method=fsync --max_connections=5000' + command: 'mysqld --innodb-flush-method=fsync' maildev: image: djfarrelly/maildev diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index 1ff5850402..705da42879 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Auth; use Appwrite\Auth\Auth; -use Appwrite\Auth\Authentication; use PHPUnit\Framework\TestCase; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -14,31 +13,21 @@ use Utopia\Database\Validator\Roles; class AuthTest extends TestCase { - protected Authorization $auth; - protected Authentication $authentication; - - public function setUp(): void - { - parent::setUp(); - $this->auth = new Authorization(); - $this->authentication = new Authentication(); - } - /** * Reset Roles */ public function tearDown(): void { - $this->auth->cleanRoles(); - $this->auth->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); } public function testCookieName(): void { $name = 'cookie-name'; - $this->assertEquals($this->authentication->setCookieName($name), $name); - $this->assertEquals($this->authentication->getCookieName(), $name); + $this->assertEquals(Auth::setCookieName($name), $name); + $this->assertEquals(Auth::$cookieName, $name); } public function testEncodeDecodeSession(): void @@ -358,7 +347,7 @@ class AuthTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertCount(1, $roles); $this->assertContains(Role::guests()->toString(), $roles); } @@ -394,7 +383,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertCount(13, $roles); $this->assertContains(Role::users()->toString(), $roles); @@ -415,21 +404,21 @@ class AuthTest extends TestCase $user['emailVerification'] = false; $user['phoneVerification'] = false; - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles); // Enable single verification type $user['emailVerification'] = true; - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); } public function testPrivilegedUserRoles(): void { - $this->auth->addRole(Auth::USER_ROLE_OWNER); + Authorization::setRole(Auth::USER_ROLE_OWNER); $user = new Document([ '$id' => ID::custom('123'), 'emailVerification' => true, @@ -455,7 +444,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); @@ -473,7 +462,7 @@ class AuthTest extends TestCase public function testAppUserRoles(): void { - $this->auth->addRole(Auth::USER_ROLE_APPS); + Authorization::setRole(Auth::USER_ROLE_APPS); $user = new Document([ '$id' => ID::custom('123'), 'memberships' => [ @@ -497,7 +486,7 @@ class AuthTest extends TestCase ] ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $this->assertCount(7, $roles); $this->assertNotContains(Role::users()->toString(), $roles); diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index 38534b9b16..dd9833378f 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -66,13 +66,9 @@ class EventTest extends TestCase $this->assertEquals('eventValue1', $this->object->getParam('eventKey1')); $this->assertEquals('eventValue2', $this->object->getParam('eventKey2')); $this->assertEquals(null, $this->object->getParam('eventKey3')); - - global $registry; - $pools = $registry->get('pools'); - $dsn = $pools['pools-queue-queue']['dsn']; - $queue = new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()); - - $client = new Client($this->object->getQueue(), $queue); + global $register; + $pools = $register->get('pools'); + $client = new Client($this->object->getQueue(), $pools->get('queue')->pop()->getResource()); $this->assertEquals($client->getQueueSize(), 1); } diff --git a/tests/unit/GraphQL/BuilderTest.php b/tests/unit/GraphQL/BuilderTest.php index f11045f318..d79a104c90 100644 --- a/tests/unit/GraphQL/BuilderTest.php +++ b/tests/unit/GraphQL/BuilderTest.php @@ -4,10 +4,8 @@ namespace Tests\Unit\GraphQL; use Appwrite\GraphQL\Types\Mapper; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Models; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; -use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class BuilderTest extends TestCase { @@ -15,9 +13,8 @@ class BuilderTest extends TestCase public function setUp(): void { - Models::init(); - $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); - Mapper::init(Models::getModels()); + $this->response = new Response(new SwooleResponse()); + Mapper::init($this->response->getModels()); } /** @@ -25,7 +22,7 @@ class BuilderTest extends TestCase */ public function testCreateTypeMapping() { - $model = Models::getModel(Response::MODEL_COLLECTION); + $model = $this->response->getModel(Response::MODEL_COLLECTION); $type = Mapper::model(\ucfirst($model->getType())); $this->assertEquals('Collection', $type->name); } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 77b3cee7d6..8ba0374093 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Authorization as ValidatorAuthorization; class MessagingChannelsTest extends TestCase { @@ -35,12 +34,8 @@ class MessagingChannelsTest extends TestCase 'functions.1', ]; - protected ValidatorAuthorization $auth; - public function setUp(): void { - $this->auth = new ValidatorAuthorization(); - /** * Setup global Counts */ @@ -71,7 +66,7 @@ class MessagingChannelsTest extends TestCase ] ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -95,7 +90,7 @@ class MessagingChannelsTest extends TestCase '$id' => '' ]); - $roles = Auth::getRoles($user, $this->auth); + $roles = Auth::getRoles($user); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php index 8043ec9f94..536278d55b 100644 --- a/tests/unit/Migration/MigrationTest.php +++ b/tests/unit/Migration/MigrationTest.php @@ -36,7 +36,7 @@ abstract class MigrationTest extends TestCase */ public function testMigrationVersions(): void { - //require_once __DIR__ . '/../../../app/init.php'; + require_once __DIR__ . '/../../../app/init.php'; foreach (Migration::$versions as $class) { $this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class)); diff --git a/tests/unit/Utopia/RequestTest.php b/tests/unit/Utopia/RequestTest.php index 6124a7a0c1..73daaa88bc 100644 --- a/tests/unit/Utopia/RequestTest.php +++ b/tests/unit/Utopia/RequestTest.php @@ -7,8 +7,7 @@ use PHPUnit\Framework\TestCase; use Swoole\Http\Request as SwooleRequest; use Tests\Unit\Utopia\Request\Filters\First; use Tests\Unit\Utopia\Request\Filters\Second; -use Utopia\Http\Adapter\Swoole\Request as UtopiaSwooleRequest; -use Utopia\Http\Route; +use Utopia\Route; class RequestTest extends TestCase { @@ -16,7 +15,7 @@ class RequestTest extends TestCase public function setUp(): void { - $this->request = new Request(new UtopiaSwooleRequest(new SwooleRequest())); + $this->request = new Request(new SwooleRequest()); } public function testFilters(): void @@ -37,7 +36,7 @@ class RequestTest extends TestCase // set test header to prevent header populaten inside the request class $this->request->addHeader('EXAMPLE', 'VALUE'); $this->request->setRoute($route); - $this->request->setQuery([ + $this->request->setQueryString([ 'initial' => true, 'first' => false ]); diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index 67253e48a0..452119fafb 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -3,14 +3,12 @@ namespace Tests\Unit\Utopia; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Models; use Exception; use PHPUnit\Framework\TestCase; use Swoole\Http\Response as SwooleResponse; use Tests\Unit\Utopia\Response\Filters\First; use Tests\Unit\Utopia\Response\Filters\Second; use Utopia\Database\Document; -use Utopia\Http\Adapter\Swoole\Response as UtopiaSwooleResponse; class ResponseTest extends TestCase { @@ -18,10 +16,10 @@ class ResponseTest extends TestCase public function setUp(): void { - $this->response = new Response(new UtopiaSwooleResponse(new SwooleResponse())); - Models::setModel(new Single()); - Models::setModel(new Lists()); - Models::setModel(new Nested()); + $this->response = new Response(new SwooleResponse()); + $this->response->setModel(new Single()); + $this->response->setModel(new Lists()); + $this->response->setModel(new Nested()); } public function testFilters(): void From c8cbcfefbb708d4d1c2a2c91e361147d61bec895 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 8 Oct 2024 23:01:54 +1300 Subject: [PATCH 239/241] Update database --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index c176d383f3..5174e98e6b 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.53.5", + "utopia-php/database": "0.53.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index d2c82a65d7..9400119ca7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1884e3a2966762c4a955842426b64f6c", + "content-hash": "df0694d5a7ed8bf57e00dd30a4d0d421", "packages": [ { "name": "adhocore/jwt", @@ -1724,16 +1724,16 @@ }, { "name": "utopia-php/database", - "version": "0.53.5", + "version": "0.53.6", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d" + "reference": "feddc8e808eaea9a11c65cca3f01683def422f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/689ba22063bf46def385da8695ba7a921e81e38d", - "reference": "689ba22063bf46def385da8695ba7a921e81e38d", + "url": "https://api.github.com/repos/utopia-php/database/zipball/feddc8e808eaea9a11c65cca3f01683def422f52", + "reference": "feddc8e808eaea9a11c65cca3f01683def422f52", "shasum": "" }, "require": { @@ -1774,9 +1774,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.53.5" + "source": "https://github.com/utopia-php/database/tree/0.53.6" }, - "time": "2024-09-24T08:43:10+00:00" + "time": "2024-10-08T02:18:46+00:00" }, { "name": "utopia-php/domains", From 325c9d61cc1c1274ace8d573fef00360dbf3ae66 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 8 Oct 2024 23:02:55 +1300 Subject: [PATCH 240/241] Lock --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5174e98e6b..50881bec87 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/cache": "0.10.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.53.*", + "utopia-php/database": "0.53.6", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index 9400119ca7..df1d2c8380 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "df0694d5a7ed8bf57e00dd30a4d0d421", + "content-hash": "a81b2ddbd465059b986947b63435e2bc", "packages": [ { "name": "adhocore/jwt", From a6a67301a415a9f5dd272b603da07fd3af84ab3b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 8 Oct 2024 23:09:19 +1300 Subject: [PATCH 241/241] Update packages --- composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index df1d2c8380..bba2391d2c 100644 --- a/composer.lock +++ b/composer.lock @@ -1624,16 +1624,16 @@ }, { "name": "utopia-php/cli", - "version": "0.15.0", + "version": "0.15.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea" + "reference": "d69bbe51a6a94dc4e5bcdd542b5938038b985a65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", - "reference": "ccb7c8125ffe0254fef8f25744bfa376eb7bd0ea", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/d69bbe51a6a94dc4e5bcdd542b5938038b985a65", + "reference": "d69bbe51a6a94dc4e5bcdd542b5938038b985a65", "shasum": "" }, "require": { @@ -1667,9 +1667,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.15.0" + "source": "https://github.com/utopia-php/cli/tree/0.15.1" }, - "time": "2023-03-01T05:55:14+00:00" + "time": "2024-10-04T13:55:36+00:00" }, { "name": "utopia-php/config", @@ -2175,16 +2175,16 @@ }, { "name": "utopia-php/migration", - "version": "0.6.4", + "version": "0.6.5", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c" + "reference": "7b2d40d526b82e9b92a17ea681b8103222e3c86a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", - "reference": "e43ef283f1386084e11d1ffe093fb6c6d7a6ce6c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/7b2d40d526b82e9b92a17ea681b8103222e3c86a", + "reference": "7b2d40d526b82e9b92a17ea681b8103222e3c86a", "shasum": "" }, "require": { @@ -2225,9 +2225,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.6.4" + "source": "https://github.com/utopia-php/migration/tree/0.6.5" }, - "time": "2024-10-02T15:16:36+00:00" + "time": "2024-10-07T08:54:05+00:00" }, { "name": "utopia-php/mongo", @@ -3002,16 +3002,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.22", + "version": "0.39.23", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "bdbb1607527550e67283ff0533522d1410c2c0df" + "reference": "0acceabb7593c9c07c5db85a84a5ebac60896763" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bdbb1607527550e67283ff0533522d1410c2c0df", - "reference": "bdbb1607527550e67283ff0533522d1410c2c0df", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0acceabb7593c9c07c5db85a84a5ebac60896763", + "reference": "0acceabb7593c9c07c5db85a84a5ebac60896763", "shasum": "" }, "require": { @@ -3047,9 +3047,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.39.22" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.23" }, - "time": "2024-10-01T16:16:26+00:00" + "time": "2024-10-08T00:38:57+00:00" }, { "name": "doctrine/annotations",