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/scopes/project.php b/app/config/scopes/project.php index 7a61524b87..6c7281f4b9 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -298,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" => @@ -351,12 +361,4 @@ 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', - ], ]; diff --git a/app/init/constants.php b/app/init/constants.php index ccf446676e..8bc9b6a826 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -52,6 +52,7 @@ 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 9530b4b98b..8276e2561e 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; @@ -297,6 +299,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()); @@ -330,6 +333,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 4a06e7b02a..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.6", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "b164631404ec759f8c368fe2321f44c22bc258ab" + "reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/b164631404ec759f8c368fe2321f44c22bc258ab", - "reference": "b164631404ec759f8c368fe2321f44c22bc258ab", + "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.6" + "source": "https://github.com/utopia-php/migration/tree/1.10.0" }, - "time": "2026-04-30T08:11:07+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/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/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/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/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/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 66c2cd7c1c..962bc8948a 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/Response.php b/src/Appwrite/Utopia/Response.php index 899cdc086a..e02e70b3d8 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -72,6 +72,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'; @@ -95,6 +96,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/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/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/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 2621240900..4212edb9df 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1257,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'); @@ -1574,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'], @@ -1624,6 +1636,7 @@ trait MigrationsBase 'mediumtext' => 'mediumText', 'longtext' => 'longText', 'varchar' => 'varchar', + 'bigint' => 2147483648 + $i, ] ]); @@ -1712,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..a55cc559b2 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']); @@ -2573,7 +2634,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 +2647,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 +2677,7 @@ trait OAuth2Base Client::METHOD_GET, '/project/oauth2', $headers, + $params, ); } } 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/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); + } +}