diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 80bb717423..37fbcc8ca3 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1523,6 +1523,13 @@ return [ 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => ID::custom('_key_team_confirm'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['teamInternalId', 'confirm'], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 6195c11724..748211f222 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -404,6 +404,13 @@ $platformCollections = [ 'lengths' => [], 'orders' => [], ], + [ + '$id' => ID::custom('_key_teamInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['teamInternalId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], ], ], @@ -635,6 +642,13 @@ $platformCollections = [ 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => ID::custom('_key_project_id'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['projectId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], ], ], @@ -1007,7 +1021,14 @@ $platformCollections = [ 'attributes' => ['projectInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], - ] + ], + [ + '$id' => ID::custom('_key_project_id'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['projectId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], ], ], diff --git a/app/config/errors.php b/app/config/errors.php index 97daae0ca6..e763c82cf2 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -623,6 +623,11 @@ return [ 'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.', 'code' => 408, ], + Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT => [ + 'name' => Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT, + 'description' => 'Asynchronous function execution timed out. Ensure the execution duration doesn\'t exceed the configured function timeout.', + 'code' => 408, + ], Exception::FUNCTION_TEMPLATE_NOT_FOUND => [ 'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND, 'description' => 'Function Template with the requested ID could not be found.', @@ -687,6 +692,11 @@ return [ 'description' => 'Build with the requested ID failed. Please check the logs for more information.', 'code' => 400, ], + Exception::BUILD_TIMEOUT => [ + 'name' => Exception::BUILD_TIMEOUT, + 'description' => 'Build timed out. Increase the build timeout via the `_APP_COMPUTE_BUILD_TIMEOUT` environment variable, or simplify the build to complete within the limit.', + 'code' => 408, + ], /** Deployments */ Exception::DEPLOYMENT_NOT_FOUND => [ diff --git a/app/config/roles.php b/app/config/roles.php index 4658a750aa..1728b7d713 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -63,8 +63,8 @@ $admins = [ 'oauth2.write', 'mocks.read', 'mocks.write', - 'policies.read', - 'policies.write', + 'project.policies.read', + 'project.policies.write', 'templates.read', 'templates.write', 'projects.write', diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index bd30da9aac..a89470c9cf 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -44,11 +44,23 @@ return [ "category" => "Project", ], "policies.read" => [ + "description" => + "Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control", + "category" => "Project", + 'deprecated' => true, + ], + "policies.write" => [ + "description" => + "Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control", + "category" => "Project", + 'deprecated' => true, + ], + "project.policies.read" => [ "description" => "Access to read project\'s policies", "category" => "Project", ], - "policies.write" => [ + "project.policies.write" => [ "description" => "Access to update project\'s policies", "category" => "Project", @@ -286,6 +298,16 @@ return [ 'category' => 'Messaging', ], + // Proxy + 'rules.read' => [ + 'description' => 'Access to read proxy rules.', + 'category' => 'Proxy', + ], + 'rules.write' => [ + 'description' => 'Access to create, update, and delete proxy rules.', + 'category' => 'Proxy', + ], + // Other "webhooks.read" => [ "description" => @@ -339,14 +361,6 @@ return [ 'description' => 'Access to create, update, and delete resources under VCS service.', 'category' => 'Other', ], - 'rules.read' => [ - 'description' => 'Access to read proxy rules.', - 'category' => 'Other', - ], - 'rules.write' => [ - 'description' => 'Access to create, update, and delete proxy rules.', - 'category' => 'Other', - ], 'presences.read' => [ 'description' => 'Access to read your project\'s presences', ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c6a5fd6f97..65fd0bfb83 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -830,11 +830,11 @@ Http::patch('/v1/account/sessions/:sessionId') $refreshToken = $session->getAttribute('providerRefreshToken', ''); $oAuthProviders = Config::getParam('oAuthProviders') ?? []; $className = $oAuthProviders[$provider]['class'] ?? null; - if (!empty($provider) && ($className === null || !\class_exists($className))) { + if (!empty($refreshToken) && ($className === null || !\class_exists($className))) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } - if (!empty($provider) && \class_exists($className)) { + if ($className !== null && \class_exists($className)) { $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; diff --git a/app/controllers/general.php b/app/controllers/general.php index eb4899a3d8..bc63d200d7 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -28,6 +28,7 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21; use Appwrite\Utopia\Request\Filters\V22 as RequestV22; use Appwrite\Utopia\Request\Filters\V23 as RequestV23; use Appwrite\Utopia\Request\Filters\V24 as RequestV24; +use Appwrite\Utopia\Request\Filters\V25 as RequestV25; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -38,7 +39,9 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21; use Appwrite\Utopia\Response\Filters\V22 as ResponseV22; use Appwrite\Utopia\Response\Filters\V23 as ResponseV23; use Appwrite\Utopia\Response\Filters\V24 as ResponseV24; +use Appwrite\Utopia\Response\Filters\V25 as ResponseV25; use Appwrite\Utopia\View; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use MaxMind\Db\Reader; use Swoole\Http\Request as SwooleRequest; @@ -579,26 +582,30 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S 'site' => '', }; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $resource->getAttribute('timeout', 30), - image: $runtime['image'], - source: $source, - entrypoint: $entrypoint, - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $runtimeEntrypoint, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $resource->getAttribute('logging', true), - requestTimeout: 30, - responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $resource->getAttribute('timeout', 30), + image: $runtime['image'], + source: $source, + entrypoint: $entrypoint, + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $runtimeEntrypoint, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $resource->getAttribute('logging', true), + requestTimeout: 30, + responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th); + } $headerOverrides = []; @@ -904,6 +911,9 @@ Http::init() if (version_compare($requestFormat, '1.9.3', '<')) { $request->addFilter(new RequestV24()); } + if (version_compare($requestFormat, '1.9.4', '<')) { + $request->addFilter(new RequestV25()); + } } $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); @@ -928,6 +938,9 @@ Http::init() */ $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { + if (version_compare($responseFormat, '1.9.4', '<')) { + $response->addFilter(new ResponseV25()); + } if (version_compare($responseFormat, '1.9.3', '<')) { $response->addFilter(new ResponseV24()); } diff --git a/app/init/constants.php b/app/init/constants.php index 787968536c..91a6dc73f9 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -44,14 +44,15 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 4324; -const APP_VERSION_STABLE = '1.9.3'; +const APP_CACHE_BUSTER = 4325; +const APP_VERSION_STABLE = '1.9.4'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime'; const APP_DATABASE_ATTRIBUTE_URL = 'url'; const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange'; +const APP_DATABASE_ATTRIBUTE_BIGINT_RANGE = 'bigintRange'; const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange'; const APP_DATABASE_ATTRIBUTE_POINT = 'point'; const APP_DATABASE_ATTRIBUTE_LINE = 'line'; diff --git a/app/init/database/formats.php b/app/init/database/formats.php index 29a4f0c7d4..9ecf07716a 100644 --- a/app/init/database/formats.php +++ b/app/init/database/formats.php @@ -36,6 +36,13 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { return new Range($min, $max, Range::TYPE_INTEGER); }, Database::VAR_INTEGER); +// BigInt uses a dedicated bigintRange format name to avoid clobbering `intRange`. +Structure::addFormat(APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, function ($attribute) { + $min = $attribute['formatOptions']['min'] ?? -INF; + $max = $attribute['formatOptions']['max'] ?? INF; + return new Range($min, $max, Range::TYPE_INTEGER); +}, Database::VAR_BIGINT); + Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { $min = $attribute['formatOptions']['min'] ?? -INF; $max = $attribute['formatOptions']['max'] ?? INF; diff --git a/app/init/models.php b/app/init/models.php index a06cc0f944..7e70452ca1 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -11,6 +11,7 @@ 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\AttributeBigInt; use Appwrite\Utopia\Response\Model\AttributeBoolean; use Appwrite\Utopia\Response\Model\AttributeDatetime; use Appwrite\Utopia\Response\Model\AttributeEmail; @@ -37,6 +38,7 @@ use Appwrite\Utopia\Response\Model\Branch; use Appwrite\Utopia\Response\Model\Bucket; use Appwrite\Utopia\Response\Model\Collection; use Appwrite\Utopia\Response\Model\Column; +use Appwrite\Utopia\Response\Model\ColumnBigInt; use Appwrite\Utopia\Response\Model\ColumnBoolean; use Appwrite\Utopia\Response\Model\ColumnDatetime; use Appwrite\Utopia\Response\Model\ColumnEmail; @@ -300,6 +302,7 @@ Response::setModel(new Attribute()); Response::setModel(new AttributeList()); Response::setModel(new AttributeString()); Response::setModel(new AttributeInteger()); +Response::setModel(new AttributeBigInt()); Response::setModel(new AttributeFloat()); Response::setModel(new AttributeBoolean()); Response::setModel(new AttributeEmail()); @@ -333,6 +336,7 @@ Response::setModel(new Column()); Response::setModel(new ColumnList()); Response::setModel(new ColumnString()); Response::setModel(new ColumnInteger()); +Response::setModel(new ColumnBigInt()); Response::setModel(new ColumnFloat()); Response::setModel(new ColumnBoolean()); Response::setModel(new ColumnEmail()); diff --git a/composer.json b/composer.json index 236e3aa83d..9a84be6111 100644 --- a/composer.json +++ b/composer.json @@ -74,13 +74,13 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.22.*", - "utopia-php/migration": "1.9.*", + "utopia-php/migration": "1.*", "utopia-php/platform": "0.13.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.17.*", - "utopia-php/servers": "0.3.*", + "utopia-php/queue": "0.18.*", + "utopia-php/servers": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "2.*", "utopia-php/system": "0.10.*", diff --git a/composer.lock b/composer.lock index de52fe7854..bbf0d59a96 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": "2af4d953af2a624be8bf2f89ea27336c", + "content-hash": "ec2ad489c60f0102f0dfab223b6d1fe4", "packages": [ { "name": "adhocore/jwt", @@ -2708,16 +2708,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342" + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342", + "url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", "shasum": "" }, "require": { @@ -2785,7 +2785,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.8" + "source": "https://github.com/symfony/http-client/tree/v7.4.9" }, "funding": [ { @@ -2805,7 +2805,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-04-29T13:25:15+00:00" }, { "name": "symfony/http-client-contracts", @@ -3658,21 +3658,21 @@ }, { "name": "utopia-php/cli", - "version": "0.23.2", + "version": "0.23.3", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab" + "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab", - "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db", + "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/servers": "0.3.*" + "utopia-php/servers": "0.4.0" }, "require-dev": { "laravel/pint": "1.2.*", @@ -3703,9 +3703,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.23.2" + "source": "https://github.com/utopia-php/cli/tree/0.23.3" }, - "time": "2026-04-27T09:19:04+00:00" + "time": "2026-05-05T04:38:59+00:00" }, { "name": "utopia-php/compression", @@ -3850,22 +3850,23 @@ }, { "name": "utopia-php/database", - "version": "5.4.2", + "version": "5.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "a1bb3e2a4fba13032ea625b21a21039c43cffeda" + "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/a1bb3e2a4fba13032ea625b21a21039c43cffeda", - "reference": "a1bb3e2a4fba13032ea625b21a21039c43cffeda", + "url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511", + "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-mongodb": "*", "ext-pdo": "*", + "ext-redis": "*", "php": ">=8.4", "utopia-php/cache": "1.*", "utopia-php/console": "0.1.*", @@ -3903,9 +3904,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.4.2" + "source": "https://github.com/utopia-php/database/tree/5.7.0" }, - "time": "2026-04-30T09:59:57+00:00" + "time": "2026-05-06T01:04:08+00:00" }, { "name": "utopia-php/detector", @@ -4271,23 +4272,23 @@ }, { "name": "utopia-php/http", - "version": "0.34.24", + "version": "0.34.25", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33" + "reference": "76be330d4197bae680eb4ccc29c573456fe91904" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33", - "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33", + "url": "https://api.github.com/repos/utopia-php/http/zipball/76be330d4197bae680eb4ccc29c573456fe91904", + "reference": "76be330d4197bae680eb4ccc29c573456fe91904", "shasum": "" }, "require": { "php": ">=8.3", "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", - "utopia-php/servers": "0.3.*", + "utopia-php/servers": "0.4.0", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4321,9 +4322,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.34.24" + "source": "https://github.com/utopia-php/http/tree/0.34.25" }, - "time": "2026-04-24T12:16:53+00:00" + "time": "2026-05-05T04:39:15+00:00" }, { "name": "utopia-php/image", @@ -4530,16 +4531,16 @@ }, { "name": "utopia-php/migration", - "version": "1.9.5", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "952a4dfe232702f80e45c35129466a8d8cb4c599" + "reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599", - "reference": "952a4dfe232702f80e45c35129466a8d8cb4c599", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/55f4863d690e775f44fec3cae4bd1f4491fed5ea", + "reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea", "shasum": "" }, "require": { @@ -4579,9 +4580,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.9.5" + "source": "https://github.com/utopia-php/migration/tree/1.10.0" }, - "time": "2026-04-29T11:19:13+00:00" + "time": "2026-05-06T04:35:32+00:00" }, { "name": "utopia-php/mongo", @@ -4646,26 +4647,26 @@ }, { "name": "utopia-php/platform", - "version": "0.13.0", + "version": "0.13.2", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "d23af5349a7ea9ee11f9920a13626226f985522e" + "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e", - "reference": "d23af5349a7ea9ee11f9920a13626226f985522e", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9", + "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", - "php": ">=8.1", - "utopia-php/cli": "0.23.*", - "utopia-php/http": "0.34.*", - "utopia-php/queue": "0.17.*", - "utopia-php/servers": "0.3.*" + "php": ">=8.3", + "utopia-php/cli": "0.23.3", + "utopia-php/http": "0.34.25", + "utopia-php/queue": "0.18.2", + "utopia-php/servers": "0.4.0" }, "require-dev": { "laravel/pint": "1.2.*", @@ -4691,9 +4692,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.13.0" + "source": "https://github.com/utopia-php/platform/tree/0.13.2" }, - "time": "2026-04-17T09:57:18+00:00" + "time": "2026-05-05T06:00:26+00:00" }, { "name": "utopia-php/pools", @@ -4803,25 +4804,24 @@ }, { "name": "utopia-php/queue", - "version": "0.17.0", + "version": "0.18.2", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f" + "reference": "f85ca003c99ff475708c05466643d067403c0c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f", - "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22", + "reference": "f85ca003c99ff475708c05466643d067403c0c22", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", "utopia-php/di": "0.3.*", - "utopia-php/fetch": "0.5.*", "utopia-php/pools": "1.*", - "utopia-php/servers": "0.3.*", + "utopia-php/servers": "0.4.0", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4864,9 +4864,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.17.0" + "source": "https://github.com/utopia-php/queue/tree/0.18.2" }, - "time": "2026-03-23T16:21:31+00:00" + "time": "2026-05-05T04:38:59+00:00" }, { "name": "utopia-php/registry", @@ -4922,16 +4922,16 @@ }, { "name": "utopia-php/servers", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "235be31200df9437fc96a1c270ffef4c64fafe52" + "reference": "7db346ef377503efe0acafe0791085270cd9ed70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52", - "reference": "235be31200df9437fc96a1c270ffef4c64fafe52", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70", + "reference": "7db346ef377503efe0acafe0791085270cd9ed70", "shasum": "" }, "require": { @@ -4970,9 +4970,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.3.0" + "source": "https://github.com/utopia-php/servers/tree/0.4.0" }, - "time": "2026-03-13T11:31:42+00:00" + "time": "2026-05-05T04:08:30+00:00" }, { "name": "utopia-php/span", @@ -5020,16 +5020,16 @@ }, { "name": "utopia-php/storage", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "8a2e3a86fd01aaed675884146665308c2122264e" + "reference": "64e132a3768e22243eda36fe4262da22fd204f3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e", - "reference": "8a2e3a86fd01aaed675884146665308c2122264e", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/64e132a3768e22243eda36fe4262da22fd204f3c", + "reference": "64e132a3768e22243eda36fe4262da22fd204f3c", "shasum": "" }, "require": { @@ -5066,22 +5066,22 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/2.0.1" + "source": "https://github.com/utopia-php/storage/tree/2.0.2" }, - "time": "2026-04-29T09:05:48+00:00" + "time": "2026-05-01T15:06:16+00:00" }, { "name": "utopia-php/system", - "version": "0.10.1", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "7c1669533bb9c285de19191270c8c1439161a78a" + "reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a", - "reference": "7c1669533bb9c285de19191270c8c1439161a78a", + "url": "https://api.github.com/repos/utopia-php/system/zipball/04229a822b147c1abaf1a92fb42c2d7aad4625df", + "reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df", "shasum": "" }, "require": { @@ -5122,9 +5122,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.10.1" + "source": "https://github.com/utopia-php/system/tree/0.10.2" }, - "time": "2026-03-15T21:07:41+00:00" + "time": "2026-05-05T14:33:41+00:00" }, { "name": "utopia-php/telemetry", @@ -5466,16 +5466,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.25.1", + "version": "1.27.5", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "f21a556b9acdbf75bbdcdc90a078af641646eade" + "reference": "9faa38b48d422f3da764a719712905c83b3922cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade", - "reference": "f21a556b9acdbf75bbdcdc90a078af641646eade", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb", + "reference": "9faa38b48d422f3da764a719712905c83b3922cb", "shasum": "" }, "require": { @@ -5511,9 +5511,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/1.25.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.27.5" }, - "time": "2026-04-28T11:12:22+00:00" + "time": "2026-05-05T12:09:40+00:00" }, { "name": "brianium/paratest", @@ -6620,16 +6620,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.23", + "version": "12.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", "shasum": "" }, "require": { @@ -6698,7 +6698,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" }, "funding": [ { @@ -6706,7 +6706,7 @@ "type": "other" } ], - "time": "2026-04-18T06:12:49+00:00" + "time": "2026-05-01T04:21:04+00:00" }, { "name": "sebastian/cli-parser", @@ -7691,16 +7691,16 @@ }, { "name": "symfony/console", - "version": "v8.0.8", + "version": "v8.0.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7" + "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7", + "url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d", + "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d", "shasum": "" }, "require": { @@ -7757,7 +7757,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.8" + "source": "https://github.com/symfony/console/tree/v8.0.9" }, "funding": [ { @@ -7777,7 +7777,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-04-29T15:02:55+00:00" }, { "name": "symfony/polyfill-ctype", diff --git a/docs/references/databases/create-bigint-attribute.md b/docs/references/databases/create-bigint-attribute.md new file mode 100644 index 0000000000..6fb607304b --- /dev/null +++ b/docs/references/databases/create-bigint-attribute.md @@ -0,0 +1 @@ +Create a bigint attribute. Optionally, minimum and maximum values can be provided. diff --git a/docs/references/databases/update-bigint-attribute.md b/docs/references/databases/update-bigint-attribute.md new file mode 100644 index 0000000000..4a301c2216 --- /dev/null +++ b/docs/references/databases/update-bigint-attribute.md @@ -0,0 +1 @@ +Update a bigint attribute. Changing the `default` value will not update already existing documents. diff --git a/docs/references/tablesdb/create-bigint-column.md b/docs/references/tablesdb/create-bigint-column.md new file mode 100644 index 0000000000..7bbbb5aac6 --- /dev/null +++ b/docs/references/tablesdb/create-bigint-column.md @@ -0,0 +1 @@ +Create a bigint column. Optionally, minimum and maximum values can be provided. diff --git a/docs/references/tablesdb/update-bigint-column.md b/docs/references/tablesdb/update-bigint-column.md new file mode 100644 index 0000000000..0dde070f6f --- /dev/null +++ b/docs/references/tablesdb/update-bigint-column.md @@ -0,0 +1 @@ +Update a bigint column. Changing the `default` value will not update already existing rows. diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 57bc9432a2..ea54c1dc65 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -178,6 +178,7 @@ class Exception extends \Exception public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing'; public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; + public const string FUNCTION_ASYNCHRONOUS_TIMEOUT = 'function_asynchronous_timeout'; public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing'; @@ -192,6 +193,7 @@ class Exception extends \Exception public const string BUILD_ALREADY_COMPLETED = 'build_already_completed'; public const string BUILD_CANCELED = 'build_canceled'; public const string BUILD_FAILED = 'build_failed'; + public const string BUILD_TIMEOUT = 'build_timeout'; /** Execution */ public const string EXECUTION_NOT_FOUND = 'execution_not_found'; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 1606c7ab40..4e5203b13f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -241,6 +241,10 @@ abstract class Action extends UtopiaAction ? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER : UtopiaResponse::MODEL_COLUMN_INTEGER, + Database::VAR_BIGINT => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT + : UtopiaResponse::MODEL_COLUMN_BIGINT, + Database::VAR_FLOAT => $isCollections ? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT : UtopiaResponse::MODEL_COLUMN_FLOAT, @@ -540,6 +544,7 @@ abstract class Action extends UtopiaAction switch ($attribute->getAttribute('format')) { case APP_DATABASE_ATTRIBUTE_INT_RANGE: + case APP_DATABASE_ATTRIBUTE_BIGINT_RANGE: case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE: $min ??= $attribute->getAttribute('formatOptions')['min']; $max ??= $attribute->getAttribute('formatOptions')['max']; @@ -548,14 +553,15 @@ abstract class Action extends UtopiaAction throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value'); } - if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) { - $validator = new Range($min, $max, Database::VAR_INTEGER); - } else { + if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_FLOAT_RANGE) { $validator = new Range($min, $max, Database::VAR_FLOAT); if (!is_null($default)) { $default = \floatval($default); } + } else { + // intRange and bigintRange share the same integer range semantics + $validator = new Range($min, $max, Range::TYPE_INTEGER); } if (!is_null($default) && !$validator->isValid($default)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php new file mode 100644 index 0000000000..4ea85b71e6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -0,0 +1,117 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint') + ->desc('Create bigint attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') + ->label('audits.event', 'attribute.create') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/databases/create-bigint-attribute.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ], + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.createBigIntColumn', + ), + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.', true) + ->param('array', false, new Boolean(), 'Is attribute an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + { + $min ??= \PHP_INT_MIN; + $max ??= \PHP_INT_MAX; + + if ($min > $max) { + throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value'); + } + + $validator = new Range($min, $max, Range::TYPE_INTEGER); + if (!\is_null($default) && !$validator->isValid($default)) { + throw new Exception($this->getInvalidValueException(), $validator->getDescription()); + } + + $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ + 'key' => $key, + 'type' => Database::VAR_BIGINT, + 'size' => 8, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, + 'formatOptions' => ['min' => $min, 'max' => $max], + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + + $formatOptions = $attribute->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $attribute->setAttribute('min', \intval($formatOptions['min'])); + $attribute->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php new file mode 100644 index 0000000000..5d8e8bf3a5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php @@ -0,0 +1,106 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint/:key') + ->desc('Update bigint attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update') + ->label('audits.event', 'attribute.update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/databases/update-bigint-attribute.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.updateBigIntColumn', + ), + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.') + ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject']) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void + { + $attribute = $this->updateAttribute( + databaseId: $databaseId, + collectionId: $collectionId, + key: $key, + dbForProject: $dbForProject, + queueForEvents: $queueForEvents, + authorization: $authorization, + type: Database::VAR_BIGINT, + default: $default, + required: $required, + min: $min, + max: $max, + newKey: $newKey + ); + + $formatOptions = $attribute->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $attribute->setAttribute('min', \intval($formatOptions['min'])); + $attribute->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index fd309a413c..3a53a49579 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -290,13 +290,15 @@ class Create extends Action } if (isset($attribute['min']) || isset($attribute['max'])) { - $format = $type === Database::VAR_INTEGER - ? APP_DATABASE_ATTRIBUTE_INT_RANGE - : APP_DATABASE_ATTRIBUTE_FLOAT_RANGE; + $format = match($type) { + Database::VAR_INTEGER => APP_DATABASE_ATTRIBUTE_INT_RANGE, + Database::VAR_BIGINT => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, + default => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, + }; $formatOptions = [ - 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX), - 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX), + 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX), + 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX), ]; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php new file mode 100644 index 0000000000..1d32c6bad9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -0,0 +1,70 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint') + ->desc('Create bigint column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/create-bigint-column.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ] + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php new file mode 100644 index 0000000000..b2754a2b7d --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -0,0 +1,71 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key') + ->desc('Update bigint column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/update-bigint-column.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.') + ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject']) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php index a8d2205236..a2fba9efb3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php @@ -2,6 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as CreateBigIntAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as UpdateBigIntAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as CreateBooleanAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as UpdateBooleanAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as CreateDatetimeAttribute; @@ -171,6 +173,10 @@ class Legacy extends Base $service->addAction(CreateIntegerAttribute::getName(), new CreateIntegerAttribute()); $service->addAction(UpdateIntegerAttribute::getName(), new UpdateIntegerAttribute()); + // Attribute: BigInt + $service->addAction(CreateBigIntAttribute::getName(), new CreateBigIntAttribute()); + $service->addAction(UpdateBigIntAttribute::getName(), new UpdateBigIntAttribute()); + // Attribute: IP $service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute()); $service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute()); diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php index 965e0929fb..765fbd4421 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php @@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Create as CreateTablesDatabase; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Delete as DeleteTablesDatabase; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Get as GetTablesDatabase; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Create as CreateBigInt; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Update as UpdateBigInt; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Create as CreateBoolean; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Update as UpdateBoolean; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Datetime\Create as CreateDatetime; @@ -151,6 +153,10 @@ class TablesDB extends Base $service->addAction(CreateInteger::getName(), new CreateInteger()); $service->addAction(UpdateInteger::getName(), new UpdateInteger()); + // Column: BigInt + $service->addAction(CreateBigInt::getName(), new CreateBigInt()); + $service->addAction(UpdateBigInt::getName(), new UpdateBigInt()); + // Column: IP $service->addAction(CreateIP::getName(), new CreateIP()); $service->addAction(UpdateIP::getName(), new UpdateIP()); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 9f15cf9d1e..02dd76294e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -17,6 +17,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Usage\Context; use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use MaxMind\Db\Reader; use Utopia\Auth\Proofs\Token; @@ -417,25 +418,29 @@ class Create extends Base $source = $deployment->getAttribute('buildPath', ''); $extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz'; $command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\""; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $source, - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $source, + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + requestTimeout: 30 + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th); + } $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index fee5b0095d..de572cd41e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -2,11 +2,13 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -38,6 +40,7 @@ class Create extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].create') ->label('audits.event', 'variable.create') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -56,10 +59,12 @@ class Create extends Base ] )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') @@ -69,10 +74,12 @@ class Create extends Base public function action( string $functionId, + string $variableId, string $key, string $value, bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, @@ -84,7 +91,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $variableId = ID::unique(); + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ @@ -120,6 +127,8 @@ class Create extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php index f6d77c2a0d..fa9f19ba8f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -35,6 +36,7 @@ class Delete extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'variable.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -56,6 +58,7 @@ class Delete extends Base ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('authorization') @@ -66,6 +69,7 @@ class Delete extends Base string $functionId, string $variableId, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Authorization $authorization @@ -98,6 +102,8 @@ class Delete extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php index 54d7a647a3..6413b29f82 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -38,6 +39,7 @@ class Update extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].update') ->label('audits.event', 'variable.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -57,10 +59,11 @@ class Update extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) - ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) + ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('authorization') @@ -70,10 +73,11 @@ class Update extends Base public function action( string $functionId, string $variableId, - string $key, + ?string $key, ?string $value, ?bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Authorization $authorization @@ -93,19 +97,27 @@ class Update extends Base throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); } - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) - ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + if (\is_null($key) && \is_null($value) && \is_null($secret)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID); + } + + $updates = new Document(); + + if (!\is_null($key)) { + $updates->setAttribute('key', $key); + $updates->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + } + + if (!\is_null($value)) { + $updates->setAttribute('value', $value); + } + + if (!\is_null($secret)) { + $updates->setAttribute('secret', $secret); + } try { - $dbForProject->updateDocument('variables', $variable->getId(), new Document([ - 'key' => $key, - 'value' => $value ?? $variable->getAttribute('value'), - 'secret' => $secret ?? $variable->getAttribute('secret'), - 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), - ])); + $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } @@ -125,6 +137,8 @@ class Update extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->dynamic($variable, Response::MODEL_VARIABLE); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php index 55dea3be1e..b330812b96 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php @@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\Queries\Variables; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Order as OrderException; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Base { @@ -51,22 +57,74 @@ class XList extends Base ) ) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) + ->param('queries', [], new Variables(), '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(', ', Variables::ALLOWED_ATTRIBUTES), true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); } - public function action(string $functionId, Response $response, Database $dbForProject) - { + /** + * @param array $queries + */ + public function action( + string $functionId, + array $queries, + bool $includeTotal, + Response $response, + Database $dbForProject + ) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $queries[] = Query::equal('resourceType', ['function']); + $queries[] = Query::equal('resourceInternalId', [$function->getSequence()]); + $queries[] = Query::orderAsc(); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $variableId = $cursor->getValue(); + $cursorDocument = $dbForProject->findOne('variables', [ + Query::equal('$id', [$variableId]), + Query::equal('resourceType', ['function']), + Query::equal('resourceInternalId', [$function->getSequence()]), + ]); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + try { + $variables = $dbForProject->find('variables', $queries); + $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + $response->dynamic(new Document([ - 'variables' => $function->getAttribute('vars', []), - 'total' => \count($function->getAttribute('vars', [])), + 'variables' => $variables, + 'total' => $total, ]), Response::MODEL_VARIABLE_LIST); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 352fb56e28..a0bd37732f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -10,11 +10,13 @@ use Appwrite\Event\Publisher\Screenshot; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; +use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Usage\Context; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use Swoole\Coroutine as Co; use Utopia\Cache\Cache; @@ -34,6 +36,7 @@ use Utopia\Detector\Detector\Rendering; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Span\Span; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; @@ -183,6 +186,12 @@ class Builds extends Action array $platform, int $timeout ): void { + Span::add('projectId', $project->getId()); + Span::add('resourceId', $resource->getId()); + Span::add('resourceType', $resource->getCollection()); + Span::add('deploymentId', $deployment->getId()); + Span::add('timeout', $timeout); + Console::info('Deployment action started'); $startTime = DateTime::now(); @@ -223,8 +232,12 @@ class Builds extends Action $version = $this->getVersion($resource); $runtime = $this->getRuntime($resource, $version); + Span::add('runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', '')); + Span::add('version', $version); $spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + Span::add('cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)); + Span::add('memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT)); // Realtime preparation $event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update"; @@ -720,6 +733,9 @@ class Builds extends Action ); Console::log('createRuntime finished'); + } catch (ExecutorTimeout $error) { + Console::warning('createRuntime timed out'); + $err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error); } catch (\Throwable $error) { Console::warning('createRuntime failed'); $err = $error; @@ -1147,13 +1163,11 @@ class Builds extends Action $message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_START}', '', $message); $message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_END}', '', $message); - // Combine with previous logs if deployment got past build process - $previousLogs = ''; - if (! is_null($deployment->getAttribute('buildSize', null))) { - $previousLogs = $deployment->getAttribute('buildLogs', ''); - if (! empty($previousLogs)) { - $message = $previousLogs . "\n" . $message; - } + // Append error to whatever build logs were already streamed + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $previousLogs = $deployment->getAttribute('buildLogs', ''); + if (! empty($previousLogs)) { + $message = $previousLogs . "\n" . $message; } $endTime = DateTime::now(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php index 7fdefca218..4130effe69 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php @@ -59,7 +59,7 @@ class Create extends Base ], )) ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) - ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false) + ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600) ->inject('response') ->inject('queueForEvents') ->inject('project') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index ae46a59c67..250a3e5df1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -11,7 +11,7 @@ use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; class Get extends Action { @@ -86,28 +86,28 @@ class Get extends Action ) ] )) - ->param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.') + ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider']) ->inject('response') ->inject('project') ->callback($this->action(...)); } public function action( - string $provider, + string $providerId, Response $response, Document $project, ): void { $providers = Config::getParam('oAuthProviders', []); - if (!\array_key_exists($provider, $providers) || !($providers[$provider]['enabled'] ?? false)) { + if (!\array_key_exists($providerId, $providers) || !($providers[$providerId]['enabled'] ?? false)) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } $actions = Base::getProviderActions(); - if (!isset($actions[$provider])) { + if (!isset($actions[$providerId])) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } - $updateClass = $actions[$provider]; + $updateClass = $actions[$providerId]; $action = new $updateClass(); $response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 9598ff4c43..697b306be8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -82,13 +82,13 @@ class Update extends Base 'hint' => '', ], [ - '$id' => 'tokenUrl', + '$id' => 'tokenURL', 'name' => 'Token URL', 'example' => 'https://myoauth.com/oauth2/token', 'hint' => '', ], [ - '$id' => 'userInfoUrl', + '$id' => 'userInfoURL', 'name' => 'User Info URL', 'example' => 'https://myoauth.com/oauth2/userinfo', 'hint' => '', @@ -127,8 +127,8 @@ class Update extends Base ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) ->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) ->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) - ->param('tokenUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) - ->param('userInfoUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true) + ->param('tokenURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true, aliases: ['tokenUrl']) + ->param('userInfoURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true, aliases: ['userInfoUrl']) ->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true) ->inject('response') ->inject('dbForPlatform') @@ -151,8 +151,8 @@ class Update extends Base static::getClientSecretParamName() => '', 'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '', 'authorizationURL' => $decoded['authorizationEndpoint'] ?? '', - 'tokenUrl' => $decoded['tokenEndpoint'] ?? '', - 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '', + 'tokenURL' => $decoded['tokenEndpoint'] ?? '', + 'userInfoURL' => $decoded['userInfoEndpoint'] ?? '', ]); } @@ -174,8 +174,8 @@ class Update extends Base ?string $clientSecret, ?string $wellKnownURL, ?string $authorizationURL, - ?string $tokenUrl, - ?string $userInfoUrl, + ?string $tokenURL, + ?string $userInfoURL, ?bool $enabled, Response $response, Database $dbForPlatform, @@ -201,8 +201,8 @@ class Update extends Base 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), 'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''), 'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''), - 'tokenEndpoint' => $tokenUrl ?? ($existing['tokenEndpoint'] ?? ''), - 'userInfoEndpoint' => $userInfoUrl ?? ($existing['userInfoEndpoint'] ?? ''), + 'tokenEndpoint' => $tokenURL ?? ($existing['tokenEndpoint'] ?? ''), + 'userInfoEndpoint' => $userInfoURL ?? ($existing['userInfoEndpoint'] ?? ''), ]; // When enabling, require either wellKnownEndpoint alone, or all three @@ -215,7 +215,7 @@ class Update extends Base && !empty($merged['userInfoEndpoint']); if (!$hasWellKnown && !$hasAllDiscovery) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenUrl, and userInfoUrl.'); + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenURL, and userInfoURL.'); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php index d0780e4bae..6a84868286 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php @@ -2,14 +2,21 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2; +use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Database\Document; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Queries; +use Utopia\Database\Validator\Query\Limit; +use Utopia\Database\Validator\Query\Offset; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Action { @@ -43,15 +50,28 @@ class XList extends Action ) ] )) + ->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) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('project') ->callback($this->action(...)); } + /** + * @param array $queries + */ public function action( + array $queries, + bool $includeTotal, Response $response, Document $project, ): void { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + $providers = Config::getParam('oAuthProviders', []); $actions = Base::getProviderActions(); @@ -66,8 +86,16 @@ class XList extends Action $documents[] = $action->buildReadResponse($project); } + $total = $includeTotal ? \count($documents) : 0; + + $grouped = Query::groupByType($queries); + $offset = $grouped['offset'] ?? 0; + $limit = $grouped['limit'] ?? null; + + $documents = \array_slice($documents, $offset, $limit); + $response->dynamic(new Document([ - 'total' => \count($documents), + 'total' => $total, 'providers' => $documents, ]), Response::MODEL_OAUTH2_PROVIDER_LIST); } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php index 3ffe30f1fa..21342332d9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php @@ -27,7 +27,7 @@ class Get extends Action ->setHttpPath('/v1/project/policies/:policyId') ->desc('Get project policy') ->groups(['api', 'project']) - ->label('scope', 'policies.read') + ->label('scope', ['policies.read', 'project.policies.read']) ->label('sdk', new Method( namespace: 'project', group: 'policies', diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php index c947ff225a..41a6168b07 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/memberships-privacy') ->desc('Update membership privacy policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php index e2c678abb6..d7ee99fbfe 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/password-dictionary') ->desc('Update password dictionary policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php index a8ae81caff..84861a19e1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/password-history') ->desc('Update password history policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php index 9db7cf0549..435f00fc39 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/personal-data') ->desc('Update password personal data policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php index 22b7a44b04..79653d46ad 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/session-alerts') ->desc('Update session alert policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php index ba72c93a6f..0a7f33218a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/duration') ->desc('Update session duration policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php index 8f8a959959..a1feb67346 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/session-invalidation') ->desc('Update session invalidation policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php index 382ed6f0d9..936a541249 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/max-sessions') ->desc('Update session limit policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php index 9129b81250..2b7e704853 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/limit') ->desc('Update user limit policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php index 893b28fef2..3020fa79dd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php @@ -33,7 +33,7 @@ class XList extends Action ->setHttpPath('/v1/project/policies') ->desc('List project policies') ->groups(['api', 'project']) - ->label('scope', 'policies.read') + ->label('scope', ['policies.read', 'project.policies.read']) ->label('sdk', new Method( namespace: 'project', group: 'policies', diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php index 8dbc720045..8c76ed2a8e 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php @@ -53,7 +53,7 @@ class Create extends Action ) ], )) - ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.') ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.') ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) @@ -72,7 +72,7 @@ class Create extends Action QueueEvent $queueForEvents, Database $dbForProject, ) { - $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $variable = new Document([ '$id' => $variableId, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php index 2b0ae8feb1..553fb09e54 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php @@ -51,7 +51,7 @@ class Delete extends Action ], contentType: ContentType::NONE )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php index af14148c92..d9030421d7 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php @@ -44,7 +44,7 @@ class Get extends Action ) ] )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php index 988a7c0849..6b05e19a78 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php @@ -52,7 +52,7 @@ class Update extends Action ) ] )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index a6a3e44194..6f2e40d13f 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; @@ -43,12 +44,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createAPIRule', description: <<inject('dbForPlatform') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log) - { + public function action( + string $domain, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + array $platform, + Log $log, + Authorization $authorization, + ) { $this->validateDomainRestrictions($domain, $platform); // TODO: (@Meldiron) Remove after 1.7.x migration @@ -108,7 +121,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -126,6 +139,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php index 1d5b770496..29751ff20a 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -38,12 +39,12 @@ class Delete extends Action ->label('audits.resource', 'rule/{request.ruleId}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'deleteRule', description: <<inject('dbForPlatform') ->inject('queueForDeletes') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -67,15 +69,16 @@ class Delete extends Action Document $project, Database $dbForPlatform, DeleteEvent $queueForDeletes, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization, ) { - $rule = $dbForPlatform->getDocument('rules', $ruleId); + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) { throw new Exception(Exception::RULE_NOT_FOUND); } - $dbForPlatform->deleteDocument('rules', $rule->getId()); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId())); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index 4a8bd4897e..c68574fefe 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -45,12 +46,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createFunctionRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $functionId, + string $branch, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $function = $dbForProject->getDocument('functions', $functionId); @@ -126,7 +143,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -144,6 +161,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php index b88a4ffc06..103ab1fddc 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; @@ -34,12 +35,12 @@ class Get extends Action ->label('scope', 'rules.read') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'getRule', description: <<inject('response') ->inject('project') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -58,15 +60,16 @@ class Get extends Action string $ruleId, Response $response, Document $project, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization, ) { - $rule = $dbForPlatform->getDocument('rules', $ruleId); + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) { throw new Exception(Exception::RULE_NOT_FOUND); } - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); + $certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''))); // Give priority to certificate generation logs if present if (!empty($certificate->getAttribute('logs', ''))) { @@ -75,6 +78,13 @@ class Get extends Action $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response->dynamic($rule, Response::MODEL_PROXY_RULE); } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index 5964a20772..f55405bb48 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -46,12 +47,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createRedirectRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $url, + int $statusCode, + string $resourceId, + string $resourceType, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $collection = match ($resourceType) { @@ -131,7 +150,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -149,6 +168,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index a9dfa93a49..7da9a11636 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -45,12 +46,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createSiteRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $siteId, + ?string $branch, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $site = $dbForProject->getDocument('sites', $siteId); @@ -126,7 +143,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -144,6 +161,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php similarity index 74% rename from src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php rename to src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php index 9e81f6ff18..1ad6f730b3 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/proxy/rules/:ruleId/verification') - ->desc('Update rule verification status') + ->setHttpPath('/v1/proxy/rules/:ruleId/status') + ->httpAlias('/v1/proxy/rules/:ruleId/verification') + ->desc('Update rule status') ->groups(['api', 'proxy']) ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].update') @@ -41,12 +43,12 @@ class Update extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, - name: 'updateRuleVerification', + group: 'rules', + name: 'updateRuleStatus', description: <<inject('project') ->inject('dbForPlatform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } @@ -71,9 +74,10 @@ class Update extends Action Event $queueForEvents, Document $project, Database $dbForPlatform, - Log $log + Log $log, + Authorization $authorization, ) { - $rule = $dbForPlatform->getDocument('rules', $ruleId); + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) { throw new Exception(Exception::RULE_NOT_FOUND); @@ -90,22 +94,22 @@ class Update extends Action try { $this->verifyRule($rule, $log); // Reset logs and status for the rule - $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $rule = $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ 'logs' => '', 'status' => RULE_STATUS_CERTIFICATE_GENERATING, - ])); + ]))); $certificateId = $rule->getAttribute('certificateId', ''); // Reset logs for the associated certificate. if (!empty($certificateId)) { - $certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ + $certificate = $authorization->skip(fn () => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ 'logs' => '', - ])); + ]))); } } catch (Exception $err) { - $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ '$updatedAt' => DateTime::now(), - ])); + ]))); throw $err; } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index 19daf8c8d2..999b4c8d74 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -13,6 +13,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Query\Cursor; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; @@ -39,12 +40,12 @@ class XList extends Action ->label('scope', 'rules.read') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'listRules', description: <<param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). 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(', ', Rules::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true, deprecated: true) ->inject('response') ->inject('project') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } public function action( array $queries, + bool $total, string $search, - bool $includeTotal, Response $response, Document $project, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization, ) { try { $queries = Query::parseQueries($queries); @@ -91,7 +94,7 @@ class XList extends Action } $ruleId = $cursor->getValue(); - $cursorDocument = $dbForPlatform->getDocument('rules', $ruleId); + $cursorDocument = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found."); @@ -102,9 +105,9 @@ class XList extends Action $filterQueries = Query::groupByType($queries)['filters']; - $rules = $dbForPlatform->find('rules', $queries); + $rules = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries)); foreach ($rules as $rule) { - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); + $certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''))); // Give priority to certificate generation logs if present if (!empty($certificate->getAttribute('logs', ''))) { @@ -112,11 +115,18 @@ class XList extends Action } $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); + + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } } $response->dynamic(new Document([ 'rules' => $rules, - 'total' => $includeTotal ? $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT) : 0, + 'total' => $total ? $authorization->skip(fn () => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0, ]), Response::MODEL_PROXY_RULE_LIST); } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php index 980c64cc54..b2a9de1933 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php @@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunction use Appwrite\Platform\Modules\Proxy\Http\Rules\Get as GetRule; use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule; use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule; -use Appwrite\Platform\Modules\Proxy\Http\Rules\Verification\Update as UpdateRuleVerification; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Status\Update as UpdateRuleStatus; use Appwrite\Platform\Modules\Proxy\Http\Rules\XList as ListRules; use Utopia\Platform\Service; @@ -26,6 +26,6 @@ class Http extends Service $this->addAction(GetRule::getName(), new GetRule()); $this->addAction(ListRules::getName(), new ListRules()); $this->addAction(DeleteRule::getName(), new DeleteRule()); - $this->addAction(UpdateRuleVerification::getName(), new UpdateRuleVerification()); + $this->addAction(UpdateRuleStatus::getName(), new UpdateRuleStatus()); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 04b30fbc9c..edd3412b8f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -2,11 +2,13 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -36,6 +38,7 @@ class Create extends Base ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'variables.[variableId].create') ->label('audits.event', 'variable.create') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( @@ -54,16 +57,18 @@ class Create extends Base ] )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->callback($this->action(...)); } - public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project) + public function action(string $siteId, string $variableId, string $key, string $value, bool $secret, Response $response, QueueEvent $queueForEvents, Database $dbForProject, Document $project) { $site = $dbForProject->getDocument('sites', $siteId); @@ -71,7 +76,7 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - $variableId = ID::unique(); + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ @@ -96,6 +101,8 @@ class Create extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index d61c9892cf..74c638bddc 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -33,6 +34,7 @@ class Delete extends Base ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'variable.delete') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( @@ -54,11 +56,12 @@ class Delete extends Base ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->callback($this->action(...)); } - public function action(string $siteId, string $variableId, Response $response, Database $dbForProject) + public function action(string $siteId, string $variableId, Response $response, QueueEvent $queueForEvents, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); @@ -77,6 +80,8 @@ class Delete extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index 08cdd4ac38..0ed7414b9d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -35,6 +36,7 @@ class Update extends Base ->desc('Update variable') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('event', 'variables.[variableId].update') ->label('audits.event', 'variable.update') ->label('audits.resource', 'site/{request.siteId}') ->label('resourceType', RESOURCE_TYPE_SITES) @@ -55,10 +57,11 @@ class Update extends Base )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) - ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) + ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->callback($this->action(...)); } @@ -66,10 +69,11 @@ class Update extends Base public function action( string $siteId, string $variableId, - string $key, + ?string $key, ?string $value, ?bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -87,19 +91,27 @@ class Update extends Base throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); } - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) - ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); + if (\is_null($key) && \is_null($value) && \is_null($secret)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID); + } + + $updates = new Document(); + + if (!\is_null($key)) { + $updates->setAttribute('key', $key); + $updates->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); + } + + if (!\is_null($value)) { + $updates->setAttribute('value', $value); + } + + if (!\is_null($secret)) { + $updates->setAttribute('secret', $secret); + } try { - $dbForProject->updateDocument('variables', $variable->getId(), new Document([ - 'key' => $variable->getAttribute('key'), - 'value' => $variable->getAttribute('value'), - 'secret' => $variable->getAttribute('secret'), - 'search' => $variable->getAttribute('search'), - ])); + $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } @@ -108,6 +120,8 @@ class Update extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->dynamic($variable, Response::MODEL_VARIABLE); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php index 669aa8be98..1270fe4925 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php @@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\Queries\Variables; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Order as OrderException; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Base { @@ -51,13 +57,20 @@ class XList extends Base ) ) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) + ->param('queries', [], new Variables(), '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(', ', Variables::ALLOWED_ATTRIBUTES), true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); } + /** + * @param array $queries + */ public function action( string $siteId, + array $queries, + bool $includeTotal, Response $response, Database $dbForProject ) { @@ -67,9 +80,51 @@ class XList extends Base throw new Exception(Exception::SITE_NOT_FOUND); } + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $queries[] = Query::equal('resourceType', ['site']); + $queries[] = Query::equal('resourceInternalId', [$site->getSequence()]); + $queries[] = Query::orderAsc(); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $variableId = $cursor->getValue(); + $cursorDocument = $dbForProject->findOne('variables', [ + Query::equal('$id', [$variableId]), + Query::equal('resourceType', ['site']), + Query::equal('resourceInternalId', [$site->getSequence()]), + ]); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + try { + $variables = $dbForProject->find('variables', $queries); + $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + $response->dynamic(new Document([ - 'variables' => $site->getAttribute('vars', []), - 'total' => \count($site->getAttribute('vars', [])), + 'variables' => $variables, + 'total' => $total, ]), Response::MODEL_VARIABLE_LIST); } } diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 82020b05b1..c8120bd017 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -163,6 +163,12 @@ class Specs extends Action 'description' => 'Your secret dev API key', 'in' => 'header', ], + 'Cookie' => [ + 'type' => 'apiKey', + 'name' => 'Cookie', + 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.', + 'in' => 'header', + ], 'ImpersonateUserId' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Impersonate-User-Id', @@ -219,6 +225,18 @@ class Specs extends Action 'description' => 'The user agent string of the client that made the request', 'in' => 'header', ], + 'DevKey' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Dev-Key', + 'description' => 'Your secret dev API key', + 'in' => 'header', + ], + 'Cookie' => [ + 'type' => 'apiKey', + 'name' => 'Cookie', + 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.', + 'in' => 'header', + ], 'ImpersonateUserId' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Impersonate-User-Id', @@ -272,7 +290,19 @@ class Specs extends Action 'Cookie' => [ 'type' => 'apiKey', 'name' => 'Cookie', - 'description' => 'The user cookie to authenticate with', + 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.', + 'in' => 'header', + ], + 'Session' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Session', + 'description' => 'The user session to authenticate with', + 'in' => 'header', + ], + 'DevKey' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Dev-Key', + 'description' => 'Your secret dev API key', 'in' => 'header', ], 'ImpersonateUserId' => [ diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 8167fb975d..a72b16cc23 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -10,6 +10,7 @@ use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Utopia\Response\Model\Execution; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use Utopia\Bus\Bus; use Utopia\Config\Config; @@ -565,24 +566,28 @@ class Functions extends Action Span::add('trigger', $trigger); Span::current()?->finish(); } - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deploymentId, - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $source, - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deploymentId, + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $source, + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_ASYNCHRONOUS_TIMEOUT, previous: $th); + } $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 69f72b8e27..3fd86baea9 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -338,6 +338,55 @@ class Migrations extends Action ); } + /** + * @return array + */ + protected function getAPIKeyScopes(): array + { + return [ + 'users.read', + 'users.write', + 'teams.read', + 'teams.write', + 'buckets.read', + 'buckets.write', + 'files.read', + 'files.write', + 'functions.read', + 'functions.write', + 'sites.read', + 'sites.write', + 'tokens.read', + 'tokens.write', + 'providers.read', + 'providers.write', + 'topics.read', + 'topics.write', + 'subscribers.read', + 'subscribers.write', + 'messages.read', + 'messages.write', + 'targets.read', + 'targets.write', + 'webhooks.read', + 'webhooks.write', + 'project.read', + 'project.write', + 'keys.read', + 'keys.write', + 'platforms.read', + 'platforms.write', + 'oauth2.read', + 'oauth2.write', + 'mocks.read', + 'mocks.write', + 'project.policies.read', + 'project.policies.write', + 'templates.read', + 'templates.write', + ]; + } + /** * @throws Exception */ @@ -358,48 +407,7 @@ class Migrations extends Action METRIC_NETWORK_INBOUND, METRIC_NETWORK_OUTBOUND, ], - 'scopes' => [ - 'users.read', - 'users.write', - 'teams.read', - 'teams.write', - 'buckets.read', - 'buckets.write', - 'files.read', - 'files.write', - 'functions.read', - 'functions.write', - 'sites.read', - 'sites.write', - 'tokens.read', - 'tokens.write', - 'providers.read', - 'providers.write', - 'topics.read', - 'topics.write', - 'subscribers.read', - 'subscribers.write', - 'messages.read', - 'messages.write', - 'targets.read', - 'targets.write', - 'webhooks.read', - 'webhooks.write', - 'project.read', - 'project.write', - 'keys.read', - 'keys.write', - 'platforms.read', - 'platforms.write', - 'oauth2.read', - 'oauth2.write', - 'mocks.read', - 'mocks.write', - 'policies.read', - 'policies.write', - 'templates.read', - 'templates.write', - ] + 'scopes' => $this->getAPIKeyScopes(), ]); return API_KEY_EPHEMERAL . '_' . $apiKey; diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 2817569037..e2fb383c8c 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -437,6 +437,15 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; + case \Utopia\Database\Validator\BigInt::class: + // BigInt validator reports Database::VAR_BIGINT, but OpenAPI expects scalar types. + // We expose it as int64 to keep schema consistent with Column/Attribute models. + $node['schema']['type'] = 'integer'; + $node['schema']['format'] = 'int64'; + if (!empty($param['example'])) { + $node['schema']['x-example'] = $param['example']; + } + break; case \Utopia\Validator\Boolean::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: false; diff --git a/src/Appwrite/Utopia/Database/Validator/Attributes.php b/src/Appwrite/Utopia/Database/Validator/Attributes.php index 16bf0909d2..54aaf135f9 100644 --- a/src/Appwrite/Utopia/Database/Validator/Attributes.php +++ b/src/Appwrite/Utopia/Database/Validator/Attributes.php @@ -23,6 +23,7 @@ class Attributes extends Validator protected array $supportedTypes = [ Database::VAR_STRING, Database::VAR_INTEGER, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, @@ -181,9 +182,9 @@ class Attributes extends Validator return false; } - // Validate signed only for integer/float types - if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) { - $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types"; + // Validate signed only for integer/bigint/float types + if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) { + $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer, bigint or float types"; return false; } @@ -199,10 +200,10 @@ class Attributes extends Validator return false; } - // Validate min/max range for integer/float + // Validate min/max range for integer/bigint/float if (isset($attribute['min']) || isset($attribute['max'])) { - if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) { - $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types"; + if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) { + $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer, bigint or float types"; return false; } @@ -264,7 +265,7 @@ class Attributes extends Validator if (isset($attribute['min']) || isset($attribute['max'])) { $min = $attribute['min'] ?? \PHP_INT_MIN; $max = $attribute['max'] ?? \PHP_INT_MAX; - $rangeValidator = new Range($min, $max, Database::VAR_INTEGER); + $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER); if (!$rangeValidator->isValid($attribute['default'])) { $this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max"; return false; @@ -272,6 +273,23 @@ class Attributes extends Validator } break; + case Database::VAR_BIGINT: + if (!is_int($attribute['default'])) { + $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be an integer"; + return false; + } + // Validate within range if min/max specified + if (isset($attribute['min']) || isset($attribute['max'])) { + $min = $attribute['min'] ?? \PHP_INT_MIN; + $max = $attribute['max'] ?? \PHP_INT_MAX; + $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER); + if (!$rangeValidator->isValid($attribute['default'])) { + $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be between $min and $max"; + return false; + } + } + break; + case Database::VAR_FLOAT: if (!is_float($attribute['default']) && !is_int($attribute['default'])) { $this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number"; @@ -281,7 +299,7 @@ class Attributes extends Validator if (isset($attribute['min']) || isset($attribute['max'])) { $min = $attribute['min'] ?? -\PHP_FLOAT_MAX; $max = $attribute['max'] ?? \PHP_FLOAT_MAX; - $rangeValidator = new Range($min, $max, Database::VAR_FLOAT); + $rangeValidator = new Range($min, $max, Range::TYPE_FLOAT); if (!$rangeValidator->isValid((float)$attribute['default'])) { $this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max"; return false; diff --git a/src/Appwrite/Utopia/Request/Filters/V25.php b/src/Appwrite/Utopia/Request/Filters/V25.php new file mode 100644 index 0000000000..cba70a5f7b --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V25.php @@ -0,0 +1,27 @@ +fillVariableId($content); + break; + } + + return $content; + } + + protected function fillVariableId(array $content): array + { + $content['variableId'] = $content['variableId'] ?? 'unique()'; + return $content; + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index bf4a5a059c..9f03c34632 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -75,6 +75,7 @@ class Response extends SwooleResponse public const MODEL_ATTRIBUTE_LIST = 'attributeList'; public const MODEL_ATTRIBUTE_STRING = 'attributeString'; public const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger'; + public const MODEL_ATTRIBUTE_BIGINT = 'attributeBigint'; public const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat'; public const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean'; public const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail'; @@ -98,6 +99,7 @@ class Response extends SwooleResponse public const MODEL_COLUMN_LIST = 'columnList'; public const MODEL_COLUMN_STRING = 'columnString'; public const MODEL_COLUMN_INTEGER = 'columnInteger'; + public const MODEL_COLUMN_BIGINT = 'columnBigint'; public const MODEL_COLUMN_FLOAT = 'columnFloat'; public const MODEL_COLUMN_BOOLEAN = 'columnBoolean'; public const MODEL_COLUMN_EMAIL = 'columnEmail'; diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php new file mode 100644 index 0000000000..bda98ed0d8 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -0,0 +1,34 @@ + $this->parseOAuth2Oidc($content), + Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => ($item['$id'] ?? null) === 'oidc' ? $this->parseOAuth2Oidc($item) : $item), + default => $content, + }; + } + + private function parseOAuth2Oidc(array $content): array + { + if (isset($content['tokenURL'])) { + $content['tokenUrl'] = $content['tokenURL']; + unset($content['tokenURL']); + } + + if (isset($content['userInfoURL'])) { + $content['userInfoUrl'] = $content['userInfoURL']; + unset($content['userInfoURL']); + } + + return $content; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php new file mode 100644 index 0000000000..baa93ff103 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -0,0 +1,66 @@ +addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute Key.', + 'default' => '', + 'example' => 'count', + ]) + ->addRule('type', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute type.', + 'default' => '', + 'example' => 'bigint', + ]) + ->addRule('min', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Minimum value to enforce for new documents.', + 'default' => null, + 'required' => false, + 'example' => 1, + ]) + ->addRule('max', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Maximum value to enforce for new documents.', + 'default' => null, + 'required' => false, + 'example' => 10, + ]) + ->addRule('default', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', + 'default' => null, + 'required' => false, + 'example' => 10, + ]) + ; + } + + public array $conditions = [ + 'type' => 'bigint' + ]; + + public function getName(): string + { + return 'AttributeBigInt'; + } + + public function getType(): string + { + return Response::MODEL_ATTRIBUTE_BIGINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 50189a80c3..87d1dc8b9f 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -19,6 +19,9 @@ class AttributeList extends Model ->addRule('attributes', [ 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, + // BigInt must come before Integer: response model dispatch is "first match wins", + // and Integer matches all int types (including bigint), while BigInt is more specific (size=8). + Response::MODEL_ATTRIBUTE_BIGINT, Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 4ab7de8e4d..bc4de22858 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -62,6 +62,9 @@ class Collection extends Model ->addRule('attributes', [ 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, + // BigInt must come before Integer: response model dispatch is "first match wins", + // and Integer matches all int types (including bigint), while BigInt is more specific (size=8). + Response::MODEL_ATTRIBUTE_BIGINT, Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php new file mode 100644 index 0000000000..895356dbf2 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -0,0 +1,66 @@ +addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Column Key.', + 'default' => '', + 'example' => 'count', + ]) + ->addRule('type', [ + 'type' => self::TYPE_STRING, + 'description' => 'Column type.', + 'default' => '', + 'example' => 'bigint', + ]) + ->addRule('min', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Minimum value to enforce for new documents.', + 'default' => null, + 'required' => false, + 'example' => 1, + ]) + ->addRule('max', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Maximum value to enforce for new documents.', + 'default' => null, + 'required' => false, + 'example' => 10, + ]) + ->addRule('default', [ + 'type' => self::TYPE_INTEGER, + 'format' => 'int64', + 'description' => 'Default value for column when not provided. Cannot be set when column is required.', + 'default' => null, + 'required' => false, + 'example' => 10, + ]) + ; + } + + public array $conditions = [ + 'type' => 'bigint' + ]; + + public function getName(): string + { + return 'ColumnBigInt'; + } + + public function getType(): string + { + return Response::MODEL_COLUMN_BIGINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ColumnList.php b/src/Appwrite/Utopia/Response/Model/ColumnList.php index e99223cd17..0586015e4d 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnList.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnList.php @@ -19,6 +19,9 @@ class ColumnList extends Model ->addRule('columns', [ 'type' => [ Response::MODEL_COLUMN_BOOLEAN, + // BigInt must come before Integer: response model dispatch is "first match wins", + // and Integer matches all int types (including bigint), while BigInt is more specific (size=8). + Response::MODEL_COLUMN_BIGINT, Response::MODEL_COLUMN_INTEGER, Response::MODEL_COLUMN_FLOAT, Response::MODEL_COLUMN_EMAIL, diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php index e4f0919666..0b18539423 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php @@ -42,13 +42,13 @@ class OAuth2Oidc extends OAuth2Base 'default' => '', 'example' => 'https://myoauth.com/oauth2/authorize', ]) - ->addRule('tokenUrl', [ + ->addRule('tokenURL', [ 'type' => self::TYPE_STRING, 'description' => 'OpenID Connect token endpoint URL.', 'default' => '', 'example' => 'https://myoauth.com/oauth2/token', ]) - ->addRule('userInfoUrl', [ + ->addRule('userInfoURL', [ 'type' => self::TYPE_STRING, 'description' => 'OpenID Connect user info endpoint URL.', 'default' => '', diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php index 1ff854e7ce..d5ea9ee0b7 100644 --- a/src/Appwrite/Utopia/Response/Model/Rule.php +++ b/src/Appwrite/Utopia/Response/Model/Rule.php @@ -74,7 +74,7 @@ class Rule extends Model ]) ->addRule('deploymentResourceId', [ 'type' => self::TYPE_STRING, - 'description' => 'ID deployment\'s resource. Used if type is "deployment"', + 'description' => 'ID of deployment\'s resource (site or function ID). Used if type is "deployment"', 'default' => '', 'example' => 'n3u9feiwmf', ]) @@ -86,10 +86,10 @@ class Rule extends Model ]) ->addRule('status', [ 'type' => self::TYPE_ENUM, - 'description' => 'Domain verification status. Possible values are "created", "verifying", "verified" and "unverified"', - 'default' => 'created', + 'description' => 'Domain verification status. Possible values are "unverified", "verifying", "verified"', + 'default' => 'unverified', 'example' => 'verified', - 'enum' => ['created', 'verifying', 'verified', 'unverified'], + 'enum' => ['unverified', 'verifying', 'verified'], ]) ->addRule('logs', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/Table.php b/src/Appwrite/Utopia/Response/Model/Table.php index 20cd3ccca2..f9f2804fe5 100644 --- a/src/Appwrite/Utopia/Response/Model/Table.php +++ b/src/Appwrite/Utopia/Response/Model/Table.php @@ -63,6 +63,9 @@ class Table extends Model ->addRule('columns', [ 'type' => [ Response::MODEL_COLUMN_BOOLEAN, + // BigInt must come before Integer: response model dispatch is "first match wins", + // and Integer matches all int types (including bigint), while BigInt is more specific (size=8). + Response::MODEL_COLUMN_BIGINT, Response::MODEL_COLUMN_INTEGER, Response::MODEL_COLUMN_FLOAT, Response::MODEL_COLUMN_EMAIL, diff --git a/src/Executor/Exception.php b/src/Executor/Exception.php new file mode 100644 index 0000000000..b799d22567 --- /dev/null +++ b/src/Executor/Exception.php @@ -0,0 +1,7 @@ +timeoutSeconds; + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index eb74867c9c..c570970732 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -2,9 +2,9 @@ namespace Executor; -use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Utopia\Fetch\BodyMultipart; -use Exception; +use Executor\Exception as ExecutorException; +use Executor\Exception\Timeout as ExecutorTimeout; use Utopia\System\System; class Executor @@ -104,7 +104,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -163,7 +163,7 @@ class Executor } if ($status >= 400) { - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -247,7 +247,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } $headers = $response['body']['headers'] ?? []; @@ -281,7 +281,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -401,7 +401,7 @@ class Executor $json = json_decode($responseBody, true); if ($json === null) { - throw new Exception('Failed to parse response: ' . $responseBody); + throw new ExecutorException('Failed to parse response: ' . $responseBody); } $responseBody = $json; @@ -412,9 +412,9 @@ class Executor if ($curlError) { if ($curlError == CURLE_OPERATION_TIMEDOUT) { - throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT); + throw new ExecutorTimeout('Executor request timed out', $timeout); } - throw new Exception($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus); + throw new ExecutorException($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus); } $responseHeaders['status-code'] = $responseStatus; diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index f7bb54024d..ef42e99663 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -380,6 +380,7 @@ function computeFlow(ctx) { api('GET', '/functions/runtimes', null, ctx.sessionHeaders, [200], 'functions.runtimes.list'); api('GET', '/functions/specifications', null, ctx.apiHeaders, [200], 'functions.specifications.list'); const functionVariable = api('POST', `/functions/${functionId}/variables`, { + variableId: 'unique()', key: 'BENCHMARK', value: 'true', secret: false, diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 3071ddfa2a..99219ebf99 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -173,8 +173,8 @@ trait ProjectCustom 'oauth2.write', 'mocks.read', 'mocks.write', - 'policies.read', - 'policies.write', + 'project.policies.read', + 'project.policies.write', 'templates.read', 'templates.write', ], diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index da788c3caa..160ee39e21 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4163,4 +4163,72 @@ class AccountCustomClientTest extends Scope $this->assertEquals(401, $verification3['headers']['status-code']); } + + public function testRefreshEmailPasswordSession(): void + { + $email = uniqid() . 'user@localhost.test'; + + $account = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => 'password', + ]); + + $this->assertEquals(201, $account['headers']['status-code']); + + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'password', + ]); + + $this->assertEquals(201, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['$id']); + + $sessionId = $session['body']['$id']; + $cookie = 'a_session_' . $this->getProject()['$id'] . '=' .$session['cookies']['a_session_' . $this->getProject()['$id']]; + + $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['expire']); + $expiryBefore = $session['body']['expire']; + + \sleep(3); // Small delay to ensure expiry an expand + + $session = $this->client->call(Client::METHOD_PATCH, '/account/sessions/' . $sessionId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['expire']); + $expiryAfter = $session['body']['expire']; + + $this->assertGreaterThan(\strtotime($expiryBefore), \strtotime($expiryAfter)); + + $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertEquals(\strtotime($expiryAfter), \strtotime($session['body']['expire'])); + } } diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 06044d9984..5d501486fd 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -7,8 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; use Utopia\Console; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; class FunctionsConsoleClientTest extends Scope { @@ -70,6 +72,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -82,6 +85,7 @@ class FunctionsConsoleClientTest extends Scope $secretVariable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -196,6 +200,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -208,6 +213,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -226,6 +232,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'ANOTHERTESTINGVALUE', 'secret' => false @@ -234,10 +241,47 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(409, $variable['headers']['status-code']); + // Test for invalid variableId + $variable = $this->createVariable( + $functionId, + [ + 'variableId' => '!invalid-id!', + 'key' => 'INVALID_ID_KEY', + 'value' => 'value', + ] + ); + + $this->assertEquals(400, $variable['headers']['status-code']); + + // Test for duplicate variableId + $duplicateVariableId = ID::unique(); + $variable = $this->createVariable( + $functionId, + [ + 'variableId' => $duplicateVariableId, + 'key' => 'DUP_ID_KEY_1', + 'value' => 'value1', + ] + ); + + $this->assertEquals(201, $variable['headers']['status-code']); + + $duplicate = $this->createVariable( + $functionId, + [ + 'variableId' => $duplicateVariableId, + 'key' => 'DUP_ID_KEY_2', + 'value' => 'value2', + ] + ); + + $this->assertEquals(409, $duplicate['headers']['status-code']); + // Test for invalid key $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => str_repeat("A", 256), 'value' => 'TESTINGVALUE' ] @@ -249,6 +293,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'LONGKEY', 'value' => str_repeat("#", 8193), ] @@ -283,6 +328,150 @@ class FunctionsConsoleClientTest extends Scope */ } + public function testListVariablesWithLimit(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables With Limit', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable1 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_1', + 'value' => 'limit-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_2', + 'value' => 'limit-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // List with limit of 1 + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['variables']); + $this->assertGreaterThanOrEqual(2, $response['body']['total']); + + $this->cleanupFunction($functionId); + } + + public function testListVariablesWithoutTotal(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables Without Total', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'NO_TOTAL_KEY', + 'value' => 'no-total-value', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // List with total=false + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'total' => false, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, $response['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($response['body']['variables'])); + + $this->cleanupFunction($functionId); + } + + public function testListVariablesCursorPagination(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables Cursor Pagination', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable1 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_1', + 'value' => 'cursor-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_2', + 'value' => 'cursor-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // Get first page with limit 1 + $page1 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page1['headers']['status-code']); + $this->assertCount(1, $page1['body']['variables']); + $cursorId = $page1['body']['variables'][0]['$id']; + + // Get next page using cursor + $page2 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page2['headers']['status-code']); + $this->assertCount(1, $page2['body']['variables']); + $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']); + + $this->cleanupFunction($functionId); + } + public function testGetVariable(): void { $data = $this->setupTestVariables(); @@ -337,6 +526,7 @@ class FunctionsConsoleClientTest extends Scope $functionId = $function['body']['$id']; $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -345,6 +535,7 @@ class FunctionsConsoleClientTest extends Scope $variableId = $variable['body']['$id']; $secretVariable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -457,6 +648,7 @@ class FunctionsConsoleClientTest extends Scope * Test for FAILURE */ + // Update with no parameters should fail with 400 $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -464,6 +656,7 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); + // Update with only value should succeed $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -471,7 +664,7 @@ class FunctionsConsoleClientTest extends Scope 'value' => 'TESTINGVALUEUPDATED_2' ]); - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(200, $response['headers']['status-code']); $longKey = str_repeat("A", 256); $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ @@ -496,6 +689,110 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); } + public function testUpdateVariableKey(): void + { + // Create a fresh function and variable for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Key', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'KEY_BEFORE', + 'value' => 'unchanged-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only key (key is nullable, but we provide a new key) + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'KEY_AFTER', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('KEY_AFTER', $response['body']['key']); + $this->assertEquals('unchanged-value', $response['body']['value']); + + $this->cleanupFunction($functionId); + } + + public function testUpdateVariableValueOnly(): void + { + // Create a fresh function and variable for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Value', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'UNCHANGED_KEY', + 'value' => 'value-before', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only value + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'value' => 'value-after', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('UNCHANGED_KEY', $response['body']['key']); + $this->assertEquals('value-after', $response['body']['value']); + + $this->cleanupFunction($functionId); + } + + public function testUpdateVariableNotFound(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Not Found', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/non-existent-id', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'NEW_KEY', + 'value' => 'new-value', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('variable_not_found', $response['body']['type']); + + $this->cleanupFunction($functionId); + } + public function testDeleteVariable(): void { // Create a fresh function and variables for this test since it deletes them @@ -512,6 +809,7 @@ class FunctionsConsoleClientTest extends Scope $functionId = $function['body']['$id']; $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -520,6 +818,7 @@ class FunctionsConsoleClientTest extends Scope $variableId = $variable['body']['$id']; $secretVariable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -585,6 +884,7 @@ class FunctionsConsoleClientTest extends Scope // create variable $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'CUSTOM_VARIABLE', 'value' => 'a_secret_value', 'secret' => true, diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 899c0ff71f..f08b711fb2 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -53,14 +53,17 @@ class FunctionsCustomServerTest extends Scope $functionId = $function['body']['$id'] ?? ''; $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey1', 'value' => 'funcValue1', ]); $variable2 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey2', 'value' => 'funcValue2', ]); $variable3 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey3', 'value' => 'funcValue3', ]); @@ -109,6 +112,7 @@ class FunctionsCustomServerTest extends Scope // Create a variable for later tests $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); @@ -278,14 +282,17 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(10, $function['body']['timeout']); $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey1', 'value' => 'funcValue1', ]); $variable2 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey2', 'value' => 'funcValue2', ]); $variable3 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey3', 'value' => 'funcValue3', ]); @@ -521,6 +528,7 @@ class FunctionsCustomServerTest extends Scope // Create a variable for later tests $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); @@ -2011,6 +2019,7 @@ class FunctionsCustomServerTest extends Scope ]); $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'CUSTOM_VARIABLE', 'value' => 'variable' ]); diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php index ed436ad075..e8e033f353 100644 --- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php @@ -55,10 +55,10 @@ class FunctionsClientTest extends Scope $query = ' mutation createVariables($functionId: String!) { - var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") { + var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") { _id } - var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") { + var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") { _id } } diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index 572fde49bf..95b52bcbe3 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -55,10 +55,10 @@ class FunctionsServerTest extends Scope $query = ' mutation createVariables($functionId: String!) { - var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") { + var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") { _id } - var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") { + var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") { _id } } diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 4346e5a5fa..4212edb9df 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1096,6 +1096,7 @@ trait MigrationsBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-appwrite-response-format' => '1.9.3' ], [ 'key' => 'TEST_VAR', 'value' => 'test_value', @@ -1256,7 +1257,6 @@ trait MigrationsBase 'max' => 65, 'required' => true, ]); - $this->assertEquals(202, $response['headers']['status-code']); $this->assertEquals($response['body']['key'], 'age'); $this->assertEquals($response['body']['type'], 'integer'); @@ -1573,6 +1573,19 @@ trait MigrationsBase $this->assertEquals(202, $varchar['headers']['status-code']); + $bigint = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/bigint', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'bigint', + 'min' => 2147483648, + 'max' => 9223372036854775807, + 'required' => false, + ]); + + $this->assertEquals(202, $bigint['headers']['status-code']); + $mediumtext = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/mediumtext', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1623,6 +1636,7 @@ trait MigrationsBase 'mediumtext' => 'mediumText', 'longtext' => 'longText', 'varchar' => 'varchar', + 'bigint' => 2147483648 + $i, ] ]); @@ -1711,6 +1725,8 @@ trait MigrationsBase $this->assertStringContainsString('mediumText', $csvData, 'CSV should contain the medium column header'); $this->assertStringContainsString('longText', $csvData, 'CSV should contain the long text column header'); $this->assertStringContainsString('varchar', $csvData, 'CSV should contain the varchar column header'); + $this->assertStringContainsString('bigint', $csvData, 'CSV should contain the bigint column header'); + $this->assertStringContainsString('2147483649', $csvData, 'CSV should contain bigint test data'); // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5451435c3c..47d92a2a58 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Project; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use Tests\E2E\Client; +use Utopia\Database\Query; trait OAuth2Base { @@ -172,6 +173,40 @@ trait OAuth2Base $this->assertNotContains('mock-unverified', $ids); } + public function testListOAuth2ProvidersTotalFalse(): void + { + $response = $this->listOAuth2Providers(total: false); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['total']); + $this->assertGreaterThan(0, \count($response['body']['providers'])); + } + + public function testListOAuth2ProvidersWithLimit(): void + { + $response = $this->listOAuth2Providers([ + Query::limit(1)->toString(), + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['providers']); + $this->assertGreaterThan(1, $response['body']['total']); + } + + public function testListOAuth2ProvidersWithOffset(): void + { + $listAll = $this->listOAuth2Providers(); + $this->assertSame(200, $listAll['headers']['status-code']); + + $listOffset = $this->listOAuth2Providers([ + Query::offset(1)->toString(), + ]); + + $this->assertSame(200, $listOffset['headers']['status-code']); + $this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']); + $this->assertSame($listAll['body']['total'], $listOffset['body']['total']); + } + // ========================================================================= // Get OAuth2 provider // ========================================================================= @@ -188,6 +223,28 @@ trait OAuth2Base $this->assertSame('', $response['body']['clientSecret']); } + public function testGetOAuth2ProviderWithAlias(): void + { + // The action declares the canonical param name as `providerId` and + // registers `provider` as an alias so that older SDK versions that + // send the provider in the query string continue to work. + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + $headers = \array_merge($headers, $this->getHeaders()); + + // Call with `provider` in query string (legacy behaviour) + $response = $this->client->call( + Client::METHOD_GET, + '/project/oauth2/github?provider=github', + $headers, + ); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('github', $response['body']['$id']); + } + public function testGetOAuth2ProviderClientSecretWriteOnly(): void { $this->updateOAuth2('amazon', [ @@ -221,19 +278,23 @@ trait OAuth2Base public function testGetOAuth2ProviderUnsupported(): void { + // The `providerId` param is validated by a WhiteList of registered + // OAuth2 provider keys, so an unknown value is rejected at validation + // time — before the action runs — and surfaces as a generic argument + // error rather than `project_provider_unsupported`. $response = $this->getOAuth2Provider('not-a-real-provider'); $this->assertSame(400, $response['headers']['status-code']); - $this->assertSame('project_provider_unsupported', $response['body']['type']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void { - // `mock` is present in oAuthProviders config (enabled: true) but is NOT - // registered in Base::getProviderActions(). Get::action has two - // separate `unsupported` throw branches — testGetOAuth2ProviderUnsupported - // covers the first (provider missing from config); this covers the - // second (provider in config but missing from the action registry). + // `mock` is present in oAuthProviders config (enabled: true) but is + // NOT registered in Base::getProviderActions(). It passes the + // WhiteList validator (which only checks config membership) and + // reaches the action body, where the action-registry check throws + // `project_provider_unsupported`. $response = $this->getOAuth2Provider('mock'); $this->assertSame(400, $response['headers']['status-code']); @@ -1590,8 +1651,8 @@ trait OAuth2Base $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']); $this->assertArrayHasKey('authorizationURL', $response['body']); - $this->assertArrayHasKey('tokenUrl', $response['body']); - $this->assertArrayHasKey('userInfoUrl', $response['body']); + $this->assertArrayHasKey('tokenURL', $response['body']); + $this->assertArrayHasKey('userInfoURL', $response['body']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1599,8 +1660,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1611,15 +1672,15 @@ trait OAuth2Base 'clientId' => 'oidc-discovery', 'clientSecret' => 'oidc-discovery-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => false, ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1627,8 +1688,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1640,8 +1701,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1670,8 +1731,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1679,7 +1740,7 @@ trait OAuth2Base 'clientId' => 'oidc-partial', 'clientSecret' => 'oidc-partial-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'tokenURL' => 'https://idp.example.com/oauth2/token', 'enabled' => true, ]); @@ -1692,8 +1753,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1724,8 +1785,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1755,8 +1816,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1770,8 +1831,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1780,7 +1841,7 @@ trait OAuth2Base 'clientId' => 'oidc-split-discovery', 'clientSecret' => 'oidc-split-discovery-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'tokenURL' => 'https://idp.example.com/oauth2/token', 'enabled' => false, ]); @@ -1788,19 +1849,19 @@ trait OAuth2Base // state must include the two stored URLs + the new one to satisfy // the all-three-discovery-URLs branch of the enable check. $enable = $this->updateOAuth2('oidc', [ - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => true, ]); $this->assertSame(200, $enable['headers']['status-code']); $this->assertTrue($enable['body']['enabled']); // Confirm all three URLs ended up persisted (merge wrote the new - // userInfoUrl while preserving the previously stored two). + // userInfoURL while preserving the previously stored two). $get = $this->getOAuth2Provider('oidc'); $this->assertSame(200, $get['headers']['status-code']); $this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1808,8 +1869,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1822,8 +1883,8 @@ trait OAuth2Base 'clientSecret' => 'oidc-clear-then-enable-secret', 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1846,8 +1907,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1868,16 +1929,16 @@ trait OAuth2Base $switch = $this->updateOAuth2('oidc', [ 'wellKnownURL' => '', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => true, ]); $this->assertSame(200, $switch['headers']['status-code']); $this->assertTrue($switch['body']['enabled']); $this->assertSame('', $switch['body']['wellKnownURL']); $this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1885,8 +1946,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1900,23 +1961,23 @@ trait OAuth2Base 'clientSecret' => 'oidc-clear-secret', 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => false, ]); $response = $this->updateOAuth2('oidc', [ 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('', $response['body']['wellKnownURL']); $this->assertSame('', $response['body']['authorizationURL']); - $this->assertSame('', $response['body']['tokenUrl']); - $this->assertSame('', $response['body']['userInfoUrl']); + $this->assertSame('', $response['body']['tokenURL']); + $this->assertSame('', $response['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1926,6 +1987,96 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void + { + // Reset to clean state + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenURL' => '', + 'userInfoURL' => '', + 'enabled' => false, + ]); + + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', + ]; + $headers = \array_merge($headers, $this->getHeaders()); + + // Update using OLD param names (aliases must still work) + $response = $this->client->call( + Client::METHOD_PATCH, + '/project/oauth2/oidc', + $headers, + [ + 'clientId' => 'oidc-compat-client', + 'clientSecret' => 'oidc-compat-secret', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => false, + ], + ); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('tokenUrl', $response['body']); + $this->assertArrayHasKey('userInfoUrl', $response['body']); + $this->assertArrayNotHasKey('tokenURL', $response['body']); + $this->assertArrayNotHasKey('userInfoURL', $response['body']); + $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']); + + // GET with 1.9.3 format must also return old param names + $get = $this->client->call( + Client::METHOD_GET, + '/project/oauth2/oidc', + $headers, + ); + + $this->assertSame(200, $get['headers']['status-code']); + $this->assertArrayHasKey('tokenUrl', $get['body']); + $this->assertArrayHasKey('userInfoUrl', $get['body']); + $this->assertArrayNotHasKey('tokenURL', $get['body']); + $this->assertArrayNotHasKey('userInfoURL', $get['body']); + $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']); + + // LIST with 1.9.3 format must also return old param names for OIDC + $list = $this->client->call( + Client::METHOD_GET, + '/project/oauth2', + $headers, + ); + + $this->assertSame(200, $list['headers']['status-code']); + $oidcEntry = null; + foreach ($list['body']['providers'] as $provider) { + if ($provider['$id'] === 'oidc') { + $oidcEntry = $provider; + break; + } + } + $this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response'); + $this->assertArrayHasKey('tokenUrl', $oidcEntry); + $this->assertArrayHasKey('userInfoUrl', $oidcEntry); + $this->assertArrayNotHasKey('tokenURL', $oidcEntry); + $this->assertArrayNotHasKey('userInfoURL', $oidcEntry); + $this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'tokenURL' => '', + 'userInfoURL' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Okta (clientId + clientSecret + optional domain/authServer) // ========================================================================= @@ -2573,7 +2724,7 @@ trait OAuth2Base ); } - protected function getOAuth2Provider(string $provider, bool $authenticated = true): mixed + protected function getOAuth2Provider(string $providerId, bool $authenticated = true): mixed { $headers = [ 'content-type' => 'application/json', @@ -2586,13 +2737,23 @@ trait OAuth2Base return $this->client->call( Client::METHOD_GET, - '/project/oauth2/' . $provider, + '/project/oauth2/' . $providerId, $headers, ); } - protected function listOAuth2Providers(bool $authenticated = true): mixed + protected function listOAuth2Providers(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed { + $params = []; + + if ($queries !== null) { + $params['queries'] = $queries; + } + + if ($total !== null) { + $params['total'] = $total; + } + $headers = [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2606,6 +2767,7 @@ trait OAuth2Base Client::METHOD_GET, '/project/oauth2', $headers, + $params, ); } } diff --git a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php index 9085733b70..eb08da56f2 100644 --- a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php @@ -89,6 +89,7 @@ class WebhooksCustomServerTest extends Scope $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', ], $this->getHeaders()), [ 'key' => 'key1', 'value' => 'value1', @@ -699,6 +700,7 @@ class WebhooksCustomServerTest extends Scope $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', ], $this->getHeaders()), [ 'key' => 'key1', 'value' => 'value1', diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 6936de9aff..3a9037c368 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -6813,6 +6813,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-mode' => 'admin', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ + 'variableId' => $variableId, 'key' => 'APP_TEST_' . $variableId, 'value' => 'TESTINGVALUE', 'secret' => false @@ -6832,6 +6833,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-mode' => 'admin', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ + 'variableId' => $variableId, 'key' => 'APP_TEST_' . $variableId, 'value' => 'TESTINGVALUE', 'secret' => false diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 59a853bfc8..32ee13b5f7 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -2,298 +2,753 @@ namespace Tests\E2E\Services\Proxy; -use Appwrite\ID; -use Appwrite\Tests\Async; -use CURLFile; use Tests\E2E\Client; -use Utopia\Console; +use Utopia\Database\Query; +use Utopia\System\System; trait ProxyBase { - use Async; + use ProxyHelpers; - protected function listRules(array $params = []): mixed + protected function tearDown(): void { - $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $rule; - } - - protected function createAPIRule(string $domain): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, + // Cleanup for testRuleVerification test + // Required as it uses static domain name + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1000)->toString(), + ] ]); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $ruleId = $rule['$id']; + $response = $this->deleteRule($ruleId); + $this->assertEquals(204, $response['headers']['status-code']); + } - return $rule; + if ($rules['body']['total'] > 0) { + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, count($rules['body']['rules'])); + $this->assertEquals(0, $rules['body']['total']); + } } - protected function updateRuleVerification(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'siteId' => $siteId, - 'branch' => $branch, - ]); - - return $rule; - } - - protected function getRule(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'url' => $url, - 'statusCode' => $statusCode, - 'resourceType' => $resourceType, - 'resourceId' => $resourceId, - ]); - - return $rule; - } - - protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'functionId' => $functionId, - 'branch' => $branch, - ]); - - return $rule; - } - - protected function deleteRule(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function setupAPIRule(string $domain): string + public function testCreateRule(): void { + $domain = \uniqid() . '-api.myapp.com'; $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertEquals('manual', $rule['body']['trigger']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('domain', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('redirectUrl', $rule['body']); + $this->assertArrayHasKey('redirectStatusCode', $rule['body']); + $this->assertArrayHasKey('deploymentResourceType', $rule['body']); + $this->assertArrayHasKey('deploymentId', $rule['body']); + $this->assertArrayHasKey('deploymentResourceId', $rule['body']); + $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); - return $rule['body']['$id']; - } + $ruleId = $rule['body']['$id']; - protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string - { - $rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId); + $rule = $this->createAPIRule($domain); + $this->assertEquals(409, $rule['headers']['status-code']); - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string - { - $rule = $this->createFunctionRule($domain, $functionId, $branch); - - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string - { - $rule = $this->createSiteRule($domain, $siteId, $branch); - - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function cleanupRule(string $ruleId): void - { $rule = $this->deleteRule($ruleId); - $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule)); + + $this->assertEquals(204, $rule['headers']['status-code']); } - protected function cleanupSite(string $siteId): void + public function testCreateRuleSetup(): void { - $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site)); + $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); + $this->cleanupRule($ruleId); } - protected function cleanupFunction(string $functionId): void + public function testCreateRuleApex(): void { - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function)); + $domain = \uniqid() . '.com'; + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); } - protected function setupSite(): mixed + public function testCreateRuleVcs(): void { - // Site - $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'siteId' => ID::unique(), - 'name' => 'Proxy site', - 'framework' => 'other', - 'adapter' => 'static', - 'buildRuntime' => 'static-1', - 'outputDirectory' => './', - 'buildCommand' => '', - 'installCommand' => '', - 'fallbackFile' => '', - ]); + $domain = \uniqid() . '-vcs.myapp.com'; - $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; - $siteId = $site['body']['$id']; + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); - // Deployment - $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'code' => $this->packageSite('static'), - 'activate' => 'true' - ]); + $rule = $this->createSiteRule('commit-' . $domain, $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['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'] ?? ''; + $rule = $this->createSiteRule('branch-' . $domain, $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); - $this->assertEventually(function () use ($siteId, $deploymentId) { - $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ])); - $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); - }, 120000, 500); + $rule = $this->createSiteRule('anything-' . $domain, $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); - return ['siteId' => $siteId, 'deploymentId' => $deploymentId]; + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; + $domain = \uniqid() . '-vcs.' . $sitesDomain; + + $rule = $this->createSiteRule('commit-' . $domain, $siteId); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createSiteRule('branch-' . $domain, $siteId); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createSiteRule('anything-' . $domain, $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); } - protected function setupFunction(): mixed + public function testCreateAPIRule(): void { - // Function - $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'], - ]), [ - 'functionId' => ID::unique(), - 'runtime' => 'node-22', - 'name' => 'Proxy Function', - 'entrypoint' => 'index.js', - 'commands' => '', - 'execute' => ['any'] + $domain = \uniqid() . '-api.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite.test'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(401, $response['headers']['status-code']); + + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); + + $this->cleanupRule($ruleId); + + $rule = $this->createAPIRule('http://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('https://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('wss://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule($domain . '/some-path'); + $this->assertEquals(400, $rule['headers']['status-code']); + } + + public function testCreateRedirectRule(): void + { + $domain = \uniqid() . '-redirect.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite.test'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(401, $response['headers']['status-code']); + + $siteId = $this->setupSite()['siteId']; + + $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['id']); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['id']); + + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); + + $domain = \uniqid() . '-redirect-307.custom.localhost'; + $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId); + $this->assertNotEmpty($ruleId); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite.test'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); + $this->assertEquals(307, $response['headers']['status-code']); + $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('type', ['redirect'])->toString(), + Query::equal('trigger', ['manual'])->toString(), + Query::equal('deploymentResourceType', ['site'])->toString(), + Query::equal('deploymentResourceId', [$siteId])->toString(), + ], ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); - $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)); + $this->cleanupSite($siteId); + $this->cleanupRule($ruleId); + } - $functionId = $function['body']['$id']; + public function testCreateFunctionRule(): void + { + $domain = \uniqid() . '-function.custom.localhost'; - // Deployment - $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'], - ]), [ - 'code' => $this->packageFunction('basic'), - 'activate' => 'true' - ]); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite.test'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); - $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'] ?? ''; + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(401, $response['headers']['status-code']); + + $setup = $this->setupFunction(); + $functionId = $setup['functionId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($functionId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupFunctionRule($domain, $functionId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']); + + $this->cleanupRule($ruleId); + + $this->cleanupFunction($functionId); $this->assertEventually(function () use ($functionId, $deploymentId) { - $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->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); - }, 100000, 500); + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('deploymentResourceType', ['function'])->toString(), + Query::equal('deploymentResourceId', [$functionId])->toString(), + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); - return ['functionId' => $functionId, 'deploymentId' => $deploymentId]; + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('deploymentId', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); } - private function packageSite(string $site): CURLFile + public function testCreateSiteRule(): void { - $stdout = ''; - $stderr = ''; + $domain = \uniqid() . '-site.custom.localhost'; - $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; - $tarPath = "$folderPath/code.tar.gz"; + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite.test'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); - Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr); + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(401, $response['headers']['status-code']); - if (filesize($tarPath) > 1024 * 1024 * 5) { - throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupSiteRule($domain, $siteId); + $this->assertNotEmpty($ruleId); + $rule = $this->getRule($ruleId); + $this->assertSame(200, $rule['headers']['status-code']); + $this->assertSame('unverified', $rule['body']['status']); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Contact page', $response['body']); + + // Wildcard domains automatically get verified status + $domains = [ + \uniqid() . '.sites.localhost', + \uniqid() . '.rebranded.localhost', + ]; + foreach ($domains as $domain) { + $wildcardRuleId = $this->setupSiteRule($domain, $siteId); + $this->assertNotEmpty($wildcardRuleId); + $rule = $this->getRule($wildcardRuleId); + $this->assertSame(200, $rule['headers']['status-code']); + $this->assertSame('verified', $rule['body']['status']); + $this->cleanupRule($wildcardRuleId); } - return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('trigger', ['deployment'])->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('deploymentResourceType', ['site'])->toString(), + Query::equal('deploymentResourceId', [$siteId])->toString(), + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThan(0, $rules['body']['total']); + + $this->cleanupRule($ruleId); + + $this->cleanupSite($siteId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('deploymentResourceType', ['site'])->toString(), + Query::equal('deploymentResourceId', [$siteId])->toString(), + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('deploymentId', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); } - private function packageFunction(string $function): CURLFile + public function testCreateSiteBranchRule(): void { - $stdout = ''; - $stderr = ''; + $domain = \uniqid() . '-site-branch.custom.localhost'; - $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; - $tarPath = "$folderPath/code.tar.gz"; + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; - Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr); + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); - if (filesize($tarPath) > 1024 * 1024 * 5) { - throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + $ruleId = $this->setupSiteRule($domain, $siteId, 'dev'); + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + + $this->cleanupRule($ruleId); + } + + public function testCreateFunctionBranchRule(): void + { + $domain = \uniqid() . '-function-branch.custom.localhost'; + + $setup = $this->setupFunction(); + $functionId = $setup['functionId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($functionId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev'); + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + + $this->cleanupRule($ruleId); + + $this->cleanupFunction($functionId); + } + + public function testUpdateRule(): void + { + // Create function appwrite-network domain + $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; + $domain = \uniqid() . '-cname-api.' . $functionsDomain; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create site appwrite-network domain + $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; + $domain = \uniqid() . '-cname-api.' . $sitesDomain; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create + update + $domain = \uniqid() . '-cname-api.custom.com'; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + + $ruleId = $rule['body']['$id']; + + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + + $this->cleanupRule($ruleId); + } + + public function testGetRule() + { + $domain = \uniqid() . '-get.custom.localhost'; + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertEquals('manual', $rule['body']['trigger']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('domain', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('redirectUrl', $rule['body']); + $this->assertArrayHasKey('redirectStatusCode', $rule['body']); + $this->assertArrayHasKey('deploymentResourceType', $rule['body']); + $this->assertArrayHasKey('deploymentId', $rule['body']); + $this->assertArrayHasKey('deploymentResourceId', $rule['body']); + $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); + + $this->cleanupRule($ruleId); + } + + public function testListRules() + { + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); } - return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rule1Domain = \uniqid() . '-list1.custom.localhost'; + $rule1Id = $this->setupAPIRule($rule1Domain); + $this->assertNotEmpty($rule1Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + + $this->assertEquals('manual', $rules['body']['rules'][0]['trigger']); + $this->assertArrayHasKey('$id', $rules['body']['rules'][0]); + $this->assertArrayHasKey('domain', $rules['body']['rules'][0]); + $this->assertArrayHasKey('type', $rules['body']['rules'][0]); + $this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]); + $this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]); + $this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]); + $this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]); + $this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]); + $this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]); + $this->assertArrayHasKey('logs', $rules['body']['rules'][0]); + $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]); + + $rule2Domain = \uniqid() . '-list1.custom.localhost'; + $rule2Id = $this->setupAPIRule($rule2Domain); + $this->assertNotEmpty($rule2Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(2, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('$id', [$rule1Id])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + + $rules = $this->listRules([ + 'queries' => [ + Query::orderDesc('$id')->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(2, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('domain', [$rule2Domain])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'search' => $rule1Domain, + 'queries' => [ Query::orderDesc('$createdAt')->toString() ] + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule1Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule2Domain, + 'queries' => [ Query::orderDesc('$createdAt')->toString() ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule2Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule1Id, + 'queries' => [ Query::orderDesc('$createdAt')->toString() ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule1Domain, $ruleDomains); + + $rules = $this->listRules([ + 'search' => $rule2Id, + 'queries' => [ Query::orderDesc('$createdAt')->toString() ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule2Domain, $ruleDomains); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); + } + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + } + + public function testRuleVerification(): void + { + + // 1. Site rule can verify + $site = $this->setupSite(); + $siteId = $site['siteId']; + + $rule = $this->createSiteRule('stage-site.webapp.com', $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + $this->assertNotEmpty($rule['body']['$id']); + $ruleId = $rule['body']['$id']; + + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals($ruleId, $rule['body']['$id']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + $this->cleanupSite($siteId); + + // 2. Function rule can verify + $function = $this->setupFunction(); + $functionId = $function['functionId']; + + $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + $this->cleanupRule($rule['body']['$id']); + + $rule = $this->createAPIRule('stage-site.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); + $this->cleanupRule($rule['body']['$id']); + + $this->cleanupFunction($functionId); + + // 3. Wrong A record fails to verify + $rule = $this->createAPIRule('wrong-a-webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 4. Correct A record can verify + $rule = $this->createAPIRule('webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + + // 5. Correct CNAME record can verify (no CAA record) + $rule = $this->createAPIRule('stage.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + + // 6. Missing CNAME record fails to verify + $rule = $this->createAPIRule('stage-missing-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 7. Wrong CNAME record fails to verify + $rule = $this->createAPIRule('stage-wrong-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 8. Wrong CAA record fails to verify + $rule = $this->createAPIRule('stage-wrong-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleStatus($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 9. Correct CAA record can verify + $rule = $this->createAPIRule('stage-correct-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + } + + public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void + { + $domain = \uniqid() . '-timestamp-test.webapp.com'; + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('unverified', $rule['body']['status']); + $this->assertNotEmpty($rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $initialUpdatedAt = $rule['body']['$updatedAt']; + $initiallogs = $rule['body']['logs']; + + sleep(1); + + $updatedRule = $this->updateRuleStatus($ruleId); + + $this->assertEquals(400, $updatedRule['headers']['status-code']); + $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']); + + $ruleAfterUpdate = $this->getRule($ruleId); + $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']); + $this->assertEquals('unverified', $ruleAfterUpdate['body']['status']); + $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']); + $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']); + + $initialTime = new \DateTime($initialUpdatedAt); + $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']); + $this->assertGreaterThan($initialTime, $updatedTime); + + $this->cleanupRule($ruleId); } } diff --git a/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php new file mode 100644 index 0000000000..68761f34a9 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php @@ -0,0 +1,14 @@ +listRules([ - 'queries' => [ - Query::endsWith('domain', 'webapp.com')->toString(), - Query::limit(1000)->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $ruleId = $rule['$id']; - $response = $this->deleteRule($ruleId); - $this->assertEquals(204, $response['headers']['status-code']); - } - - if ($rules['body']['total'] > 0) { - $rules = $this->listRules([ - 'queries' => [ - Query::endsWith('domain', 'webapp.com')->toString(), - Query::limit(1)->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, count($rules['body']['rules'])); - $this->assertEquals(0, $rules['body']['total']); - } - } - - public function testCreateRule(): void - { - $domain = \uniqid() . '-api.myapp.com'; - $rule = $this->createAPIRule($domain); - - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals($domain, $rule['body']['domain']); - $this->assertEquals('manual', $rule['body']['trigger']); - $this->assertArrayHasKey('$id', $rule['body']); - $this->assertArrayHasKey('domain', $rule['body']); - $this->assertArrayHasKey('type', $rule['body']); - $this->assertArrayHasKey('redirectUrl', $rule['body']); - $this->assertArrayHasKey('redirectStatusCode', $rule['body']); - $this->assertArrayHasKey('deploymentResourceType', $rule['body']); - $this->assertArrayHasKey('deploymentId', $rule['body']); - $this->assertArrayHasKey('deploymentResourceId', $rule['body']); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); - $this->assertArrayHasKey('logs', $rule['body']); - $this->assertArrayHasKey('renewAt', $rule['body']); - - $ruleId = $rule['body']['$id']; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(409, $rule['headers']['status-code']); - - $rule = $this->deleteRule($ruleId); - - $this->assertEquals(204, $rule['headers']['status-code']); - } - - public function testCreateRuleSetup(): void - { - $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); - $this->cleanupRule($ruleId); - } - - public function testCreateRuleApex(): void - { - $domain = \uniqid() . '.com'; - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - } - - public function testCreateRuleVcs(): void - { - $domain = \uniqid() . '-vcs.myapp.com'; - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $rule = $this->createSiteRule('commit-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createSiteRule('branch-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createSiteRule('anything-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; - $domain = \uniqid() . '-vcs.' . $sitesDomain; - - $rule = $this->createSiteRule('commit-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('branch-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('anything-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - } - - public function testCreateAPIRule(): void - { - $domain = \uniqid() . '-api.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/versions'); - $this->assertEquals(401, $response['headers']['status-code']); - - $ruleId = $this->setupAPIRule($domain); - - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/versions'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); - - $this->cleanupRule($ruleId); - - $rule = $this->createAPIRule('http://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule('https://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule('wss://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule($domain . '/some-path'); - $this->assertEquals(400, $rule['headers']['status-code']); - } - - public function testCreateRedirectRule(): void - { - $domain = \uniqid() . '-redirect.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); - $this->assertEquals(401, $response['headers']['status-code']); - - $siteId = $this->setupSite()['siteId']; - - $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId); - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['id']); - - $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['id']); - - $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); - $this->assertEquals(301, $response['headers']['status-code']); - $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); - - $domain = \uniqid() . '-redirect-307.custom.localhost'; - $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId); - $this->assertNotEmpty($ruleId); - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); - $this->assertEquals(307, $response['headers']['status-code']); - $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('type', ['redirect'])->toString(), - Query::equal('trigger', ['manual'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ], - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - - $this->cleanupSite($siteId); - $this->cleanupRule($ruleId); - } - - public function testCreateFunctionRule(): void - { - $domain = \uniqid() . '-function.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/ping'); - $this->assertEquals(401, $response['headers']['status-code']); - - $setup = $this->setupFunction(); - $functionId = $setup['functionId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($functionId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupFunctionRule($domain, $functionId); - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/ping'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']); - - $this->cleanupRule($ruleId); - - $this->cleanupFunction($functionId); - - $this->assertEventually(function () use ($functionId, $deploymentId) { - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['function'])->toString(), - Query::equal('deploymentResourceId', [$functionId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentId', [$deploymentId])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - }); - } - - public function testCreateSiteRule(): void - { - $domain = \uniqid() . '-site.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/contact'); - $this->assertEquals(401, $response['headers']['status-code']); - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupSiteRule($domain, $siteId); - $this->assertNotEmpty($ruleId); - $rule = $this->getRule($ruleId); - $this->assertSame(200, $rule['headers']['status-code']); - $this->assertSame('created', $rule['body']['status']); - - $response = $proxyClient->call(Client::METHOD_GET, '/contact'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertStringContainsString('Contact page', $response['body']); - - // Wildcard domains automatically get verified status - $domains = [ - \uniqid() . '.sites.localhost', - \uniqid() . '.rebranded.localhost', - ]; - foreach ($domains as $domain) { - $wildcardRuleId = $this->setupSiteRule($domain, $siteId); - $this->assertNotEmpty($wildcardRuleId); - $rule = $this->getRule($wildcardRuleId); - $this->assertSame(200, $rule['headers']['status-code']); - $this->assertSame('verified', $rule['body']['status']); - $this->cleanupRule($wildcardRuleId); - } - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('trigger', ['deployment'])->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertGreaterThan(0, $rules['body']['total']); - - $this->cleanupRule($ruleId); - - $this->cleanupSite($siteId); - - $this->assertEventually(function () use ($siteId, $deploymentId) { - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentId', [$deploymentId])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - }); - } - - public function testCreateSiteBranchRule(): void - { - $domain = \uniqid() . '-site-branch.custom.localhost'; - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupSiteRule($domain, $siteId, 'dev'); - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - } - - public function testCreateFunctionBranchRule(): void - { - $domain = \uniqid() . '-function-branch.custom.localhost'; - - $setup = $this->setupFunction(); - $functionId = $setup['functionId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($functionId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev'); - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - - $this->cleanupFunction($functionId); - } - - public function testUpdateRule(): void - { - // Create function appwrite-network domain - $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; - $domain = \uniqid() . '-cname-api.' . $functionsDomain; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verified', $rule['body']['status']); - - $this->cleanupRule($rule['body']['$id']); - - // Create site appwrite-network domain - $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; - $domain = \uniqid() . '-cname-api.' . $sitesDomain; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verified', $rule['body']['status']); - - $this->cleanupRule($rule['body']['$id']); - - // Create + update - $domain = \uniqid() . '-cname-api.custom.com'; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $ruleId = $rule['body']['$id']; - - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - } - - public function testGetRule() - { - $domain = \uniqid() . '-get.custom.localhost'; - $ruleId = $this->setupAPIRule($domain); - - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals($domain, $rule['body']['domain']); - $this->assertEquals('manual', $rule['body']['trigger']); - $this->assertArrayHasKey('$id', $rule['body']); - $this->assertArrayHasKey('domain', $rule['body']); - $this->assertArrayHasKey('type', $rule['body']); - $this->assertArrayHasKey('redirectUrl', $rule['body']); - $this->assertArrayHasKey('redirectStatusCode', $rule['body']); - $this->assertArrayHasKey('deploymentResourceType', $rule['body']); - $this->assertArrayHasKey('deploymentId', $rule['body']); - $this->assertArrayHasKey('deploymentResourceId', $rule['body']); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); - $this->assertArrayHasKey('logs', $rule['body']); - $this->assertArrayHasKey('renewAt', $rule['body']); - - $this->cleanupRule($ruleId); - } - - public function testListRules() - { - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $rule = $this->deleteRule($rule['$id']); - $this->assertEquals(204, $rule['headers']['status-code']); - } - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rule1Domain = \uniqid() . '-list1.custom.localhost'; - $rule1Id = $this->setupAPIRule($rule1Domain); - $this->assertNotEmpty($rule1Id); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); - - $this->assertEquals('manual', $rules['body']['rules'][0]['trigger']); - $this->assertArrayHasKey('$id', $rules['body']['rules'][0]); - $this->assertArrayHasKey('domain', $rules['body']['rules'][0]); - $this->assertArrayHasKey('type', $rules['body']['rules'][0]); - $this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]); - $this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]); - $this->assertArrayHasKey('logs', $rules['body']['rules'][0]); - $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]); - - $rule2Domain = \uniqid() . '-list1.custom.localhost'; - $rule2Id = $this->setupAPIRule($rule2Domain); - $this->assertNotEmpty($rule2Id); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - $this->assertCount(2, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('$id', [$rule1Id])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); - - $rules = $this->listRules([ - 'queries' => [ - Query::orderDesc('$id')->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(2, $rules['body']['rules']); - $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('domain', [$rule2Domain])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); - - $rules = $this->listRules([ - 'search' => $rule1Domain, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleIds = \array_column($rules['body']['rules'], '$id'); - $this->assertContains($rule1Id, $ruleIds); - - $rules = $this->listRules([ - 'search' => $rule2Domain, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleIds = \array_column($rules['body']['rules'], '$id'); - $this->assertContains($rule2Id, $ruleIds); - - $rules = $this->listRules([ - 'search' => $rule1Id, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleDomains = \array_column($rules['body']['rules'], 'domain'); - $this->assertContains($rule1Domain, $ruleDomains); - - $rules = $this->listRules([ - 'search' => $rule2Id, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleDomains = \array_column($rules['body']['rules'], 'domain'); - $this->assertContains($rule2Domain, $ruleDomains); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $rule = $this->deleteRule($rule['$id']); - $this->assertEquals(204, $rule['headers']['status-code']); - } - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - } - - public function testRuleVerification(): void - { - - // 1. Site rule can verify - $site = $this->setupSite(); - $siteId = $site['siteId']; - - $rule = $this->createSiteRule('stage-site.webapp.com', $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - $this->assertNotEmpty($rule['body']['$id']); - $ruleId = $rule['body']['$id']; - - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals($ruleId, $rule['body']['$id']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - $this->cleanupSite($siteId); - - // 2. Function rule can verify - $function = $this->setupFunction(); - $functionId = $function['functionId']; - - $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createAPIRule('stage-site.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); - $this->cleanupRule($rule['body']['$id']); - - $this->cleanupFunction($functionId); - - // 3. Wrong A record fails to verify - $rule = $this->createAPIRule('wrong-a-webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 4. Correct A record can verify - $rule = $this->createAPIRule('webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - - // 5. Correct CNAME record can verify (no CAA record) - $rule = $this->createAPIRule('stage.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - - // 6. Missing CNAME record fails to verify - $rule = $this->createAPIRule('stage-missing-cname.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 7. Wrong CNAME record fails to verify - $rule = $this->createAPIRule('stage-wrong-cname.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 8. Wrong CAA record fails to verify - $rule = $this->createAPIRule('stage-wrong-caa.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 9. Correct CAA record can verify - $rule = $this->createAPIRule('stage-correct-caa.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - } - - public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void - { - $domain = \uniqid() . '-timestamp-test.webapp.com'; - $rule = $this->createAPIRule($domain); - - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertNotEmpty($rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $initialUpdatedAt = $rule['body']['$updatedAt']; - $initiallogs = $rule['body']['logs']; - - sleep(1); - - $updatedRule = $this->updateRuleVerification($ruleId); - - $this->assertEquals(400, $updatedRule['headers']['status-code']); - $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']); - - $ruleAfterUpdate = $this->getRule($ruleId); - $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']); - $this->assertEquals('created', $ruleAfterUpdate['body']['status']); - $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']); - $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']); - - $initialTime = new \DateTime($initialUpdatedAt); - $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']); - $this->assertGreaterThan($initialTime, $updatedTime); - - $this->cleanupRule($ruleId); - } } diff --git a/tests/e2e/Services/Proxy/ProxyHelpers.php b/tests/e2e/Services/Proxy/ProxyHelpers.php new file mode 100644 index 0000000000..6f15abdad8 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyHelpers.php @@ -0,0 +1,293 @@ +client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $rule; + } + + protected function createAPIRule(string $domain): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + ]); + + return $rule; + } + + protected function updateRuleStatus(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/status', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'siteId' => $siteId, + 'branch' => $branch, + ]); + + return $rule; + } + + protected function getRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'url' => $url, + 'statusCode' => $statusCode, + 'resourceType' => $resourceType, + 'resourceId' => $resourceId, + ]); + + return $rule; + } + + protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'functionId' => $functionId, + 'branch' => $branch, + ]); + + return $rule; + } + + protected function deleteRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function setupAPIRule(string $domain): string + { + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string + { + $rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string + { + $rule = $this->createFunctionRule($domain, $functionId, $branch); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string + { + $rule = $this->createSiteRule($domain, $siteId, $branch); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function cleanupRule(string $ruleId): void + { + $rule = $this->deleteRule($ruleId); + $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule)); + } + + protected function cleanupSite(string $siteId): void + { + $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site)); + } + + 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'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function)); + } + + protected function setupSite(): mixed + { + // Site + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'siteId' => ID::unique(), + 'name' => 'Proxy site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + + $siteId = $site['body']['$id']; + + // Deployment + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + + $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 ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 120000, 500); + + return ['siteId' => $siteId, 'deploymentId' => $deploymentId]; + } + + protected function setupFunction(): mixed + { + // Function + $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(), + 'runtime' => 'node-22', + 'name' => 'Proxy Function', + 'entrypoint' => 'index.js', + 'commands' => '', + 'execute' => ['any'] + ]); + + $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']; + + // Deployment + $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' => $this->packageFunction('basic'), + 'activate' => 'true' + ]); + + $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) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return ['functionId' => $functionId, 'deploymentId' => $deploymentId]; + } + + private function packageSite(string $site): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr); + + 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)); + } + + private function packageFunction(string $function): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr); + + 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/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 7d9257c699..2beae74d3e 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -104,14 +104,17 @@ class SitesCustomServerTest extends Scope $this->assertEquals('./', $site['body']['outputDirectory']); $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', ]); $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', ]); $variable3 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', ]); @@ -211,6 +214,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('Test Site', $site['body']['name']); $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', 'secret' => false, @@ -223,6 +227,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(false, $variable['body']['secret']); $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', 'secret' => false, @@ -235,6 +240,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(false, $variable2['body']['secret']); $secretVariable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', 'secret' => true, @@ -330,6 +336,316 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + public function testListVariablesWithLimit(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables Limit', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable1 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_1', + 'value' => 'limit-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_2', + 'value' => 'limit-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // List with limit of 1 + $response = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['variables']); + $this->assertGreaterThanOrEqual(2, $response['body']['total']); + + $this->cleanupSite($siteId); + } + + public function testListVariablesWithoutTotal(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables No Total', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'NO_TOTAL_KEY', + 'value' => 'no-total-value', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // List with total=false + $response = $this->listVariables($siteId, [ + 'total' => false, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, $response['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($response['body']['variables'])); + + $this->cleanupSite($siteId); + } + + public function testListVariablesCursorPagination(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables Cursor', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable1 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_1', + 'value' => 'cursor-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_2', + 'value' => 'cursor-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // Get first page with limit 1 + $page1 = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page1['headers']['status-code']); + $this->assertCount(1, $page1['body']['variables']); + $cursorId = $page1['body']['variables'][0]['$id']; + + // Get next page using cursor + $page2 = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page2['headers']['status-code']); + $this->assertCount(1, $page2['body']['variables']); + $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableKey(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Key', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'KEY_BEFORE', + 'value' => 'unchanged-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only key + $response = $this->updateVariable($siteId, $variableId, [ + 'key' => 'KEY_AFTER', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('KEY_AFTER', $response['body']['key']); + $this->assertEquals('unchanged-value', $response['body']['value']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableValueOnly(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Value', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'UNCHANGED_KEY', + 'value' => 'value-before', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only value + $response = $this->updateVariable($siteId, $variableId, [ + 'value' => 'value-after', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('UNCHANGED_KEY', $response['body']['key']); + $this->assertEquals('value-after', $response['body']['value']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableNoOp(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable NoOp', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'NOOP_KEY', + 'value' => 'noop-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update with no parameters should fail with 400 + $response = $this->updateVariable($siteId, $variableId, []); + + $this->assertEquals(400, $response['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableNotFound(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Not Found', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $response = $this->updateVariable($siteId, 'non-existent-id', [ + 'key' => 'NEW_KEY', + 'value' => 'new-value', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('variable_not_found', $response['body']['type']); + + $this->cleanupSite($siteId); + } + + public function testCreateVariableInvalidId(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Invalid Variable ID', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => '!invalid-id!', + 'key' => 'INVALID_ID_KEY', + 'value' => 'value', + ]); + + $this->assertEquals(400, $variable['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + public function testCreateVariableDuplicateId(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Duplicate Variable ID', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variableId = ID::unique(); + + $variable = $this->createVariable($siteId, [ + 'variableId' => $variableId, + 'key' => 'DUP_ID_KEY_1', + 'value' => 'value1', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // Attempt to create with same ID + $duplicate = $this->createVariable($siteId, [ + 'variableId' => $variableId, + 'key' => 'DUP_ID_KEY_2', + 'value' => 'value2', + ]); + + $this->assertEquals(409, $duplicate['headers']['status-code']); + $this->assertEquals('variable_already_exists', $duplicate['body']['type']); + + $this->cleanupSite($siteId); + } + // This is first Sites test with Proxy // If this fails, it may not be related to variables; but Router flow failing public function testVariablesE2E(): void @@ -351,6 +667,7 @@ class SitesCustomServerTest extends Scope $domain = $this->setupSiteDomain($siteId); $secretVariable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'name', 'value' => 'Appwrite', ]); diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php new file mode 100644 index 0000000000..4280bfece9 --- /dev/null +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -0,0 +1,482 @@ +getProject()['$id'] ?? 'default'; + if (!empty(self::$setupCache[$cacheKey])) { + return self::$setupCache[$cacheKey]; + } + + $projectId = $this->getProject()['$id']; + $apiKey = $this->getProject()['apiKey']; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $apiKey, + ]; + + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'Numeric Types Test Database', + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [ + 'tableId' => ID::unique(), + 'name' => 'Numeric Types Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create integer column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [ + 'key' => 'integer_field', + 'required' => false, + 'min' => -10, + 'max' => 10, + 'default' => 0, + ]); + + // Create bigint column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ + 'key' => 'bigint_field', + 'required' => false, + 'min' => -9007199254740991, + 'max' => 9007199254740991, + 'default' => 9007199254740000, + ]); + + // Create unsigned integer column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [ + 'key' => 'unsigned_int_field', + 'required' => false, + 'min' => 0, + 'max' => 100, + 'default' => 0, + 'signed' => false, + ]); + + // Create unsigned bigint column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ + 'key' => 'unsigned_bigint_field', + 'required' => false, + 'min' => 0, + 'max' => 9223372036854775807, + 'default' => 0, + 'signed' => false, + ]); + + // Cache before waiting so that if waitForAllAttributes times out, + // subsequent calls don't try to re-create the same columns (causing 409) + self::$setupCache[$cacheKey] = [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + ]; + + // Wait for all columns to be available + $this->waitForAllAttributes($databaseId, $tableId); + + return self::$setupCache[$cacheKey]; + } + + /** + * Setup database/table without caching so mutations (update/delete) don't + * affect other tests that might be executed in a different order. + */ + protected function setupFreshDatabaseAndTable(): array + { + $projectId = $this->getProject()['$id']; + $apiKey = $this->getProject()['apiKey']; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $apiKey, + ]; + + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'Numeric Types Test Database', + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [ + 'tableId' => ID::unique(), + 'name' => 'Numeric Types Table', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [ + 'key' => 'integer_field', + 'required' => false, + 'min' => -10, + 'max' => 10, + 'default' => 0, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ + 'key' => 'bigint_field', + 'required' => false, + 'min' => -9007199254740991, + 'max' => 9007199254740991, + 'default' => 9007199254740000, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [ + 'key' => 'unsigned_int_field', + 'required' => false, + 'max' => 100, + 'default' => 0, + 'signed' => false, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ + 'key' => 'unsigned_bigint_field', + 'required' => false, + 'max' => 9223372036854775807, + 'default' => 0, + 'signed' => false, + ]); + + $this->waitForAllAttributes($databaseId, $tableId); + + return [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + ]; + } + + public function testCreateDatabase(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Numeric Types Test Database', + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + } + + public function testCreateTable(): void + { + $data = $this->setupDatabaseAndTable(); + + $table = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $data['databaseId'] . '/tables/' . $data['tableId'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $table['headers']['status-code']); + $this->assertEquals($data['tableId'], $table['body']['$id']); + } + + public function testGetIntegerAndBigIntColumns(): void + { + $data = $this->setupDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + $integerColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $integerColumn['headers']['status-code']); + $this->assertEquals('integer_field', $integerColumn['body']['key']); + $this->assertEquals('integer', $integerColumn['body']['type']); + $this->assertEquals(false, $integerColumn['body']['required']); + $this->assertEquals(false, $integerColumn['body']['array']); + $this->assertEquals(-10, $integerColumn['body']['min']); + $this->assertEquals(10, $integerColumn['body']['max']); + $this->assertEquals(0, $integerColumn['body']['default']); + + $bigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $bigintColumn['headers']['status-code']); + $this->assertEquals('bigint_field', $bigintColumn['body']['key']); + + $this->assertEquals('bigint', $bigintColumn['body']['type']); + $this->assertEquals(false, $bigintColumn['body']['required']); + $this->assertEquals(false, $bigintColumn['body']['array']); + $this->assertEquals(-9007199254740991, $bigintColumn['body']['min']); + $this->assertEquals(9007199254740991, $bigintColumn['body']['max']); + $this->assertEquals(9007199254740000, $bigintColumn['body']['default']); + } + + public function testGetUnsignedIntegerAndBigIntColumns(): void + { + $data = $this->setupDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + $unsignedIntColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_int_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $unsignedIntColumn['headers']['status-code']); + $this->assertEquals('unsigned_int_field', $unsignedIntColumn['body']['key']); + $this->assertEquals('integer', $unsignedIntColumn['body']['type']); + $this->assertEquals(false, $unsignedIntColumn['body']['required']); + $this->assertEquals(false, $unsignedIntColumn['body']['array']); + $this->assertEquals(false, $unsignedIntColumn['body']['signed']); + $this->assertEquals(0, $unsignedIntColumn['body']['min']); + $this->assertEquals(100, $unsignedIntColumn['body']['max']); + $this->assertEquals(0, $unsignedIntColumn['body']['default']); + + $unsignedBigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_bigint_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $unsignedBigintColumn['headers']['status-code']); + $this->assertEquals('unsigned_bigint_field', $unsignedBigintColumn['body']['key']); + $this->assertEquals('bigint', $unsignedBigintColumn['body']['type']); + $this->assertEquals(false, $unsignedBigintColumn['body']['required']); + $this->assertEquals(false, $unsignedBigintColumn['body']['array']); + $this->assertEquals(false, $unsignedBigintColumn['body']['signed']); + $this->assertEquals(0, $unsignedBigintColumn['body']['min']); + $this->assertEquals(9223372036854775807, $unsignedBigintColumn['body']['max']); + $this->assertEquals(0, $unsignedBigintColumn['body']['default']); + } + + public function testListColumnsWithNumericTypes(): void + { + $data = $this->setupDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $columns['headers']['status-code']); + $this->assertIsArray($columns['body']['columns']); + $this->assertGreaterThan(0, $columns['body']['total']); + + $columnKeys = array_map(fn ($col) => $col['key'], $columns['body']['columns']); + $this->assertContains('integer_field', $columnKeys); + $this->assertContains('bigint_field', $columnKeys); + $this->assertContains('unsigned_int_field', $columnKeys); + $this->assertContains('unsigned_bigint_field', $columnKeys); + + $columnTypeByKey = []; + foreach ($columns['body']['columns'] as $col) { + $columnTypeByKey[$col['key']] = $col['type']; + } + + $this->assertEquals('integer', $columnTypeByKey['integer_field']); + $this->assertEquals('bigint', $columnTypeByKey['bigint_field']); + $this->assertEquals('integer', $columnTypeByKey['unsigned_int_field']); + $this->assertEquals('bigint', $columnTypeByKey['unsigned_bigint_field']); + } + + public function testCreateRowWithIntegerAndBigIntTypes(): void + { + $data = $this->setupDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'rowId' => ID::unique(), + 'data' => [ + 'integer_field' => 5, + 'bigint_field' => 456, + 'unsigned_int_field' => 50, + 'unsigned_bigint_field' => 9007199254740000, + ], + 'permissions' => [ + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertEquals(5, $row['body']['integer_field']); + $this->assertEquals(456, $row['body']['bigint_field']); + $this->assertEquals(50, $row['body']['unsigned_int_field']); + $this->assertEquals(9007199254740000, $row['body']['unsigned_bigint_field']); + } + + public function testUpdateIntegerAndBigIntColumns(): void + { + $data = $this->setupFreshDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + // Update integer column + $updateInteger = $this->client->call( + Client::METHOD_PATCH, + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer/integer_field', + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], + [ + 'required' => false, + 'min' => -20, + 'max' => 20, + 'default' => 3, + ] + ); + + $this->assertEquals(200, $updateInteger['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $tableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $column['headers']['status-code']); + $this->assertEquals(-20, $column['body']['min']); + $this->assertEquals(20, $column['body']['max']); + $this->assertEquals(3, $column['body']['default']); + }, 30000, 250); + + // Update bigint column + $updateBigint = $this->client->call( + Client::METHOD_PATCH, + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint/bigint_field', + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], + [ + 'required' => false, + 'min' => -999, + 'max' => 999, + 'default' => 10, + ] + ); + + $this->assertEquals(200, $updateBigint['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $tableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $column['headers']['status-code']); + $this->assertEquals(-999, $column['body']['min']); + $this->assertEquals(999, $column['body']['max']); + $this->assertEquals(10, $column['body']['default']); + }, 30000, 250); + } + + public function testDeleteIntegerAndBigIntColumns(): void + { + $data = $this->setupFreshDatabaseAndTable(); + $databaseId = $data['databaseId']; + $tableId = $data['tableId']; + + // Delete integer column + $deleteInteger = $this->client->call( + Client::METHOD_DELETE, + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ] + ); + + $this->assertEquals(204, $deleteInteger['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $tableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $column['headers']['status-code']); + }, 30000, 250); + + // Delete bigint column + $deleteBigint = $this->client->call( + Client::METHOD_DELETE, + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ] + ); + + $this->assertEquals(204, $deleteBigint['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $tableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $column['headers']['status-code']); + }, 30000, 250); + } +}