Merge branch '1.9.x' into fix-oauth-oidc-params

This commit is contained in:
Matej Bačo
2026-05-06 11:17:42 +02:00
49 changed files with 2511 additions and 1173 deletions
+7
View File
@@ -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' => [],
],
],
],
+22 -1
View File
@@ -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],
],
],
],
+10 -8
View File
@@ -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',
],
];
+1
View File
@@ -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';
+7
View File
@@ -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;
+4
View File
@@ -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());
+3 -3
View File
@@ -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.*",
Generated
+88 -88
View File
@@ -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",
@@ -0,0 +1 @@
Create a bigint attribute. Optionally, minimum and maximum values can be provided.
@@ -0,0 +1 @@
Update a bigint attribute. Changing the `default` value will not update already existing documents.
@@ -0,0 +1 @@
Create a bigint column. Optionally, minimum and maximum values can be provided.
@@ -0,0 +1 @@
Update a bigint column. Changing the `default` value will not update already existing rows.
@@ -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)) {
@@ -0,0 +1,117 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createBigIntAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
}
public function __construct()
{
$this
->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());
}
}
@@ -0,0 +1,106 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateBigIntAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
}
public function __construct()
{
$this
->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());
}
}
@@ -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),
];
}
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as BigIntCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Create extends BigIntCreate
{
public static function getName(): string
{
return 'createBigIntColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BIGINT;
}
public function __construct()
{
$this
->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(...));
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as BigIntUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends BigIntUpdate
{
public static function getName(): string
{
return 'updateBigIntColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BIGINT;
}
public function __construct()
{
$this
->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(...));
}
}
@@ -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());
@@ -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());
@@ -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')
@@ -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());
@@ -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<string> $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);
}
@@ -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: <<<EOT
Create a new proxy rule for serving Appwrite's API on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -67,11 +70,21 @@ class Create extends Action
->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);
@@ -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: <<<EOT
Delete a proxy rule by its unique ID.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -58,6 +59,7 @@ class Delete extends Action
->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)
@@ -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: <<<EOT
Create a new proxy rule for executing Appwrite Function on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -72,11 +75,25 @@ class Create extends Action
->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);
@@ -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: <<<EOT
Get a proxy rule by its unique ID.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -51,6 +52,7 @@ class Get extends Action
->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);
}
}
@@ -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: <<<EOT
Create a new proxy rule for to redirect from custom domain to another domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -75,11 +78,27 @@ class Create extends Action
->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);
@@ -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: <<<EOT
Create a new proxy rule for serving Appwrite Site on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -72,11 +75,25 @@ class Create extends Action
->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);
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Status;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
@@ -13,6 +13,7 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -32,8 +33,9 @@ class Update extends Action
$this
->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: <<<EOT
Retry getting verification process of a proxy rule. This endpoint triggers domain verification by checking DNS records (CNAME) against the configured target domain. If verification is successful, a TLS certificate will be automatically provisioned for the domain.
If not succeeded yet, retry verification process of a proxy rule domain. This endpoint triggers domain verification by checking DNS records. If verification is successful, a TLS certificate will be automatically provisioned for the domain asynchronously in the background.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -61,6 +63,7 @@ class Update extends Action
->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;
}
@@ -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: <<<EOT
Get a list of all the proxy rules. You can use the query params to filter your results.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -53,21 +54,23 @@ class XList extends Action
]
))
->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);
}
}
@@ -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());
}
}
@@ -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;
@@ -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;
+2
View File
@@ -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';
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class AttributeBigInt extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->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;
}
}
@@ -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,
@@ -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,
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ColumnBigInt extends Column
{
public function __construct()
{
parent::__construct();
$this
->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;
}
}
@@ -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,
+4 -4
View File
@@ -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,
@@ -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,
@@ -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, [
+81 -9
View File
@@ -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,
);
}
}
+689 -234
View File
@@ -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);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ProxyConsoleClientTest extends Scope
{
use ProxyBase;
use ProjectCustom;
use SideConsole;
}
@@ -2,758 +2,13 @@
namespace Tests\E2E\Services\Proxy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Query;
use Utopia\System\System;
class ProxyCustomServerTest extends Scope
{
use ProxyBase;
use ProjectCustom;
use SideServer;
protected function tearDown(): void
{
// 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']);
}
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);
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Appwrite\ID;
use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Console;
trait ProxyHelpers
{
use Async;
protected function listRules(array $params = []): mixed
{
$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,
]);
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));
}
}
@@ -0,0 +1,482 @@
<?php
namespace Tests\E2E\Services\TablesDB;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ApiTablesDB;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SchemaPolling;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Traits\DatabasesUrlHelpers;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class DatabasesNumericTypesTest extends Scope
{
use ProjectCustom;
use SideServer;
use ApiTablesDB;
use DatabasesUrlHelpers;
use SchemaPolling;
private static array $setupCache = [];
/**
* Setup database, table, and numeric columns for parallel-safe tests.
*/
protected function setupDatabaseAndTable(): array
{
$cacheKey = $this->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);
}
}