From 03c73ac0aab0b66116065540341e904854dd7880 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 15:50:59 +0530 Subject: [PATCH 01/73] added big int attributes --- composer.json | 4 +- composer.lock | 48 ++++--- .../databases/create-bigint-attribute.md | 1 + .../databases/update-bigint-attribute.md | 1 + .../Collections/Attributes/BigInt/Create.php | 120 ++++++++++++++++++ .../Collections/Attributes/BigInt/Update.php | 106 ++++++++++++++++ .../Databases/Services/Registry/Legacy.php | 6 + 7 files changed, 268 insertions(+), 18 deletions(-) create mode 100644 docs/references/databases/create-bigint-attribute.md create mode 100644 docs/references/databases/update-bigint-attribute.md create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php diff --git a/composer.json b/composer.json index 65838a1615..ba7d89d6ed 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", - "utopia-php/database": "5.*", + "utopia-php/database": "dev-big-init as 5.7", "utopia-php/agents": "1.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", @@ -73,7 +73,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.8.*", + "utopia-php/migration": "dev-big-int as 1.8.6", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", diff --git a/composer.lock b/composer.lock index 1441bf06d1..036415e843 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f9225f2b580de0ccb796b2fb8c881384", + "content-hash": "4bdbec3c3569eac8ed04d24a71ddaa34", "packages": [ { "name": "adhocore/jwt", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.17", + "version": "dev-big-init", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993" + "reference": "ef5c77972a95129eb929cce6c8f7c674db930ec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ef5c77972a95129eb929cce6c8f7c674db930ec9", + "reference": "ef5c77972a95129eb929cce6c8f7c674db930ec9", "shasum": "" }, "require": { @@ -3903,9 +3903,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.17" + "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-03-20T01:18:52+00:00" + "time": "2026-03-30T06:31:05+00:00" }, { "name": "utopia-php/detector", @@ -4518,16 +4518,16 @@ }, { "name": "utopia-php/migration", - "version": "1.8.3", + "version": "dev-big-int", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "8633523b3343d492427331b6eec53f020f6ab7a7" + "reference": "9e8708883677661ac568ba41f7a0b79cd6f04253" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7", - "reference": "8633523b3343d492427331b6eec53f020f6ab7a7", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/9e8708883677661ac568ba41f7a0b79cd6f04253", + "reference": "9e8708883677661ac568ba41f7a0b79cd6f04253", "shasum": "" }, "require": { @@ -4536,7 +4536,7 @@ "ext-openssl": "*", "halaxa/json-machine": "^1.2", "php": ">=8.1", - "utopia-php/database": "5.*", + "utopia-php/database": "dev-big-init as 5.4", "utopia-php/dsn": "0.2.*", "utopia-php/storage": "1.0.*" }, @@ -4567,9 +4567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.8.3" + "source": "https://github.com/utopia-php/migration/tree/big-int" }, - "time": "2026-03-19T09:18:47+00:00" + "time": "2026-03-30T10:03:42+00:00" }, { "name": "utopia-php/mongo", @@ -8433,9 +8433,25 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-big-init", + "alias": "5.7", + "alias_normalized": "5.7.0.0" + }, + { + "package": "utopia-php/migration", + "version": "dev-big-int", + "alias": "1.8.6", + "alias_normalized": "1.8.6.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/database": 20, + "utopia-php/migration": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/docs/references/databases/create-bigint-attribute.md b/docs/references/databases/create-bigint-attribute.md new file mode 100644 index 0000000000..6fb607304b --- /dev/null +++ b/docs/references/databases/create-bigint-attribute.md @@ -0,0 +1 @@ +Create a bigint attribute. Optionally, minimum and maximum values can be provided. diff --git a/docs/references/databases/update-bigint-attribute.md b/docs/references/databases/update-bigint-attribute.md new file mode 100644 index 0000000000..4a301c2216 --- /dev/null +++ b/docs/references/databases/update-bigint-attribute.md @@ -0,0 +1 @@ +Update a bigint attribute. Changing the `default` value will not update already existing documents. diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php new file mode 100644 index 0000000000..ecae3c3db3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -0,0 +1,120 @@ +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.createIntegerColumn', + ), + )) + ->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, Database::VAR_INTEGER); + if (!\is_null($default) && !$validator->isValid($default)) { + throw new Exception($this->getInvalidValueException(), $validator->getDescription()); + } + + // `bigint` is always stored as a 64-bit integer. + $size = 8; + + $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ + 'key' => $key, + 'type' => Database::VAR_INTEGER, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, + 'formatOptions' => ['min' => $min, 'max' => $max], + ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + + $formatOptions = $attribute->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $attribute->setAttribute('min', \intval($formatOptions['min'])); + $attribute->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php new file mode 100644 index 0000000000..2010b51208 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php @@ -0,0 +1,106 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint/:key') + ->desc('Update bigint attribute') + ->groups(['api', 'database', 'schema']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update') + ->label('audits.event', 'attribute.update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/databases/update-bigint-attribute.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'tablesDB.updateIntegerColumn', + ), + )) + ->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_INTEGER, + default: $default, + required: $required, + min: $min, + max: $max, + newKey: $newKey + ); + + $formatOptions = $attribute->getAttribute('formatOptions', []); + if (!empty($formatOptions)) { + $attribute->setAttribute('min', \intval($formatOptions['min'])); + $attribute->setAttribute('max', \intval($formatOptions['max'])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($attribute, $this->getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php index a8d2205236..a2fba9efb3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php @@ -2,6 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as CreateBigIntAttribute; +use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as UpdateBigIntAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as CreateBooleanAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as UpdateBooleanAttribute; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as CreateDatetimeAttribute; @@ -171,6 +173,10 @@ class Legacy extends Base $service->addAction(CreateIntegerAttribute::getName(), new CreateIntegerAttribute()); $service->addAction(UpdateIntegerAttribute::getName(), new UpdateIntegerAttribute()); + // Attribute: BigInt + $service->addAction(CreateBigIntAttribute::getName(), new CreateBigIntAttribute()); + $service->addAction(UpdateBigIntAttribute::getName(), new UpdateBigIntAttribute()); + // Attribute: IP $service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute()); $service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute()); From ffa3f741ed41525b5080cd57c3ca4bbb353df73d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 15:53:06 +0530 Subject: [PATCH 02/73] added bigint columns --- .../tablesdb/create-bigint-column.md | 1 + .../tablesdb/update-bigint-column.md | 1 + .../TablesDB/Tables/Columns/BigInt/Create.php | 70 ++++++++++++++++++ .../TablesDB/Tables/Columns/BigInt/Update.php | 71 +++++++++++++++++++ .../Databases/Services/Registry/TablesDB.php | 6 ++ 5 files changed, 149 insertions(+) create mode 100644 docs/references/tablesdb/create-bigint-column.md create mode 100644 docs/references/tablesdb/update-bigint-column.md create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php diff --git a/docs/references/tablesdb/create-bigint-column.md b/docs/references/tablesdb/create-bigint-column.md new file mode 100644 index 0000000000..7bbbb5aac6 --- /dev/null +++ b/docs/references/tablesdb/create-bigint-column.md @@ -0,0 +1 @@ +Create a bigint column. Optionally, minimum and maximum values can be provided. diff --git a/docs/references/tablesdb/update-bigint-column.md b/docs/references/tablesdb/update-bigint-column.md new file mode 100644 index 0000000000..0dde070f6f --- /dev/null +++ b/docs/references/tablesdb/update-bigint-column.md @@ -0,0 +1 @@ +Update a bigint column. Changing the `default` value will not update already existing rows. diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php new file mode 100644 index 0000000000..514e13aa32 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -0,0 +1,70 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint') + ->desc('Create bigint column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create') + ->label('audits.event', 'column.create') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/create-bigint-column.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_ACCEPTED, + model: $this->getResponseModel(), + ) + ] + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.', true) + ->param('array', false, new Boolean(), 'Is column an array?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDatabase') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php new file mode 100644 index 0000000000..6f4343cbc9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -0,0 +1,71 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key') + ->desc('Update bigint column') + ->groups(['api', 'database', 'schema']) + ->label('scope', ['tables.write', 'collections.write']) + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update') + ->label('audits.event', 'column.update') + ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') + ->label('sdk', new Method( + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), + name: self::getName(), + description: '/docs/references/tablesdb/update-bigint-column.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: $this->getResponseModel(), + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) + ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject']) + ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject']) + ->param('required', null, new Boolean(), 'Is column required?') + ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true) + ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true) + ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.') + ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject']) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('authorization') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php index 965e0929fb..765fbd4421 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php @@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Create as CreateTablesDatabase; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Delete as DeleteTablesDatabase; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Get as GetTablesDatabase; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Create as CreateBigInt; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Update as UpdateBigInt; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Create as CreateBoolean; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Update as UpdateBoolean; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Datetime\Create as CreateDatetime; @@ -151,6 +153,10 @@ class TablesDB extends Base $service->addAction(CreateInteger::getName(), new CreateInteger()); $service->addAction(UpdateInteger::getName(), new UpdateInteger()); + // Column: BigInt + $service->addAction(CreateBigInt::getName(), new CreateBigInt()); + $service->addAction(UpdateBigInt::getName(), new UpdateBigInt()); + // Column: IP $service->addAction(CreateIP::getName(), new CreateIP()); $service->addAction(UpdateIP::getName(), new UpdateIP()); From 01db1efbc48bd3a30410c0ee1230db6939feeae7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 16:08:21 +0530 Subject: [PATCH 03/73] added tests --- .../TablesDB/DatabasesNumericTypesTest.php | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php new file mode 100644 index 0000000000..d341681a65 --- /dev/null +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -0,0 +1,400 @@ +getProject()['$id'] ?? 'default'; + if (!empty(static::$setupCache[$cacheKey])) { + return static::$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' => -900719925, + 'max' => 900719925, + 'default' => 123, + ]); + + // Cache before waiting so that if waitForAllAttributes times out, + // subsequent calls don't try to re-create the same columns (causing 409) + static::$setupCache[$cacheKey] = [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + ]; + + // Wait for all columns to be available + $this->waitForAllAttributes($databaseId, $tableId); + + return static::$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' => -900719925, + 'max' => 900719925, + 'default' => 123, + ]); + + $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']); + + // Some implementations may still represent bigint columns as integer internally. + $this->assertTrue(\in_array($bigintColumn['body']['type'], ['bigint', 'integer'], true)); + $this->assertEquals(false, $bigintColumn['body']['required']); + $this->assertEquals(false, $bigintColumn['body']['array']); + $this->assertEquals(-900719925, $bigintColumn['body']['min']); + $this->assertEquals(900719925, $bigintColumn['body']['max']); + $this->assertEquals(123, $bigintColumn['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); + + $columnTypeByKey = []; + foreach ($columns['body']['columns'] as $col) { + $columnTypeByKey[$col['key']] = $col['type']; + } + + $this->assertEquals('integer', $columnTypeByKey['integer_field']); + $this->assertTrue(\in_array($columnTypeByKey['bigint_field'], ['bigint', 'integer'], true)); + } + + 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, + ], + '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']); + } + + 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_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_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); + } +} From a9ed74c6a83d52e30c719a733e0bbe48f20f05ec Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 16:11:42 +0530 Subject: [PATCH 04/73] added tests to migration --- tests/e2e/Services/Migrations/MigrationsBase.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 1e8b1f5ad3..0dc62a3be9 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1572,6 +1572,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' => 0, + 'max' => 100000, + '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'], @@ -1622,6 +1635,7 @@ trait MigrationsBase 'mediumtext' => 'mediumText', 'longtext' => 'longText', 'varchar' => 'varchar', + 'bigint' => $i * 1000, ] ]); @@ -1710,6 +1724,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('1000', $csvData, 'CSV should contain bigint test data'); // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ From 77d195f0c3edfc222e1eb3a33113e98ec28023c3 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 16:17:44 +0530 Subject: [PATCH 05/73] updated response models --- app/init/models.php | 4 +++ .../Collections/Attributes/BigInt/Create.php | 2 +- .../Collections/Attributes/BigInt/Update.php | 2 +- .../TablesDB/Tables/Columns/BigInt/Create.php | 2 +- .../TablesDB/Tables/Columns/BigInt/Update.php | 2 +- src/Appwrite/Utopia/Response.php | 2 ++ .../Utopia/Response/Model/AttributeBigInt.php | 31 +++++++++++++++++++ .../Utopia/Response/Model/AttributeList.php | 1 + .../Utopia/Response/Model/Collection.php | 1 + .../Utopia/Response/Model/ColumnBigInt.php | 31 +++++++++++++++++++ .../Utopia/Response/Model/ColumnList.php | 1 + src/Appwrite/Utopia/Response/Model/Table.php | 1 + 12 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 src/Appwrite/Utopia/Response/Model/AttributeBigInt.php create mode 100644 src/Appwrite/Utopia/Response/Model/ColumnBigInt.php diff --git a/app/init/models.php b/app/init/models.php index bf6d67dd95..794401375b 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -11,6 +11,7 @@ use Appwrite\Utopia\Response\Model\AlgoScryptModified; use Appwrite\Utopia\Response\Model\AlgoSha; use Appwrite\Utopia\Response\Model\Any; use Appwrite\Utopia\Response\Model\Attribute; +use Appwrite\Utopia\Response\Model\AttributeBigInt; use Appwrite\Utopia\Response\Model\AttributeBoolean; use Appwrite\Utopia\Response\Model\AttributeDatetime; use Appwrite\Utopia\Response\Model\AttributeEmail; @@ -37,6 +38,7 @@ use Appwrite\Utopia\Response\Model\Branch; use Appwrite\Utopia\Response\Model\Bucket; use Appwrite\Utopia\Response\Model\Collection; use Appwrite\Utopia\Response\Model\Column; +use Appwrite\Utopia\Response\Model\ColumnBigInt; use Appwrite\Utopia\Response\Model\ColumnBoolean; use Appwrite\Utopia\Response\Model\ColumnDatetime; use Appwrite\Utopia\Response\Model\ColumnEmail; @@ -232,6 +234,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()); @@ -265,6 +268,7 @@ Response::setModel(new Column()); Response::setModel(new ColumnList()); Response::setModel(new ColumnString()); Response::setModel(new ColumnInteger()); +Response::setModel(new ColumnBigInt()); Response::setModel(new ColumnFloat()); Response::setModel(new ColumnBoolean()); Response::setModel(new ColumnEmail()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index ecae3c3db3..f99ee38d91 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -31,7 +31,7 @@ class Create extends Action protected function getResponseModel(): string|array { - return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER; + return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php index 2010b51208..8e4a060ace 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php @@ -28,7 +28,7 @@ class Update extends Action protected function getResponseModel(): string|array { - return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER; + return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php index 514e13aa32..1b7b33291b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -24,7 +24,7 @@ class Create extends BigIntCreate protected function getResponseModel(): string|array { - return UtopiaResponse::MODEL_COLUMN_INTEGER; + return UtopiaResponse::MODEL_COLUMN_BIGINT; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php index 6f4343cbc9..387dd08238 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -25,7 +25,7 @@ class Update extends BigIntUpdate protected function getResponseModel(): string|array { - return UtopiaResponse::MODEL_COLUMN_INTEGER; + return UtopiaResponse::MODEL_COLUMN_BIGINT; } public function __construct() diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 99170a58c9..2f36270624 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -72,6 +72,7 @@ class Response extends SwooleResponse public const MODEL_ATTRIBUTE_LIST = 'attributeList'; public const MODEL_ATTRIBUTE_STRING = 'attributeString'; public const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger'; + public const MODEL_ATTRIBUTE_BIGINT = 'attributeBigint'; public const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat'; public const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean'; public const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail'; @@ -95,6 +96,7 @@ class Response extends SwooleResponse public const MODEL_COLUMN_LIST = 'columnList'; public const MODEL_COLUMN_STRING = 'columnString'; public const MODEL_COLUMN_INTEGER = 'columnInteger'; + public const MODEL_COLUMN_BIGINT = 'columnBigint'; public const MODEL_COLUMN_FLOAT = 'columnFloat'; public const MODEL_COLUMN_BOOLEAN = 'columnBoolean'; public const MODEL_COLUMN_EMAIL = 'columnEmail'; diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php new file mode 100644 index 0000000000..9e0d918fe0 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -0,0 +1,31 @@ + self::TYPE_INTEGER, + 'size' => 8, + ]; + + public function __construct() + { + parent::__construct(); + + // Update example for the `type` field + $this->rules['type']['example'] = 'bigint'; + } + + public function getName(): string + { + return 'AttributeBigInt'; + } + + public function getType(): string + { + return Response::MODEL_ATTRIBUTE_BIGINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 50189a80c3..70459c3df1 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -20,6 +20,7 @@ class AttributeList extends Model 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, Response::MODEL_ATTRIBUTE_INTEGER, + Response::MODEL_ATTRIBUTE_BIGINT, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, Response::MODEL_ATTRIBUTE_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 4ab7de8e4d..7fe335cf09 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -63,6 +63,7 @@ class Collection extends Model 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, Response::MODEL_ATTRIBUTE_INTEGER, + Response::MODEL_ATTRIBUTE_BIGINT, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, Response::MODEL_ATTRIBUTE_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php new file mode 100644 index 0000000000..9d7649f144 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -0,0 +1,31 @@ + self::TYPE_INTEGER, + 'size' => 8, + ]; + + public function __construct() + { + parent::__construct(); + + // Update example for the `type` field + $this->rules['type']['example'] = 'bigint'; + } + + public function getName(): string + { + return 'ColumnBigInt'; + } + + public function getType(): string + { + return Response::MODEL_COLUMN_BIGINT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ColumnList.php b/src/Appwrite/Utopia/Response/Model/ColumnList.php index e99223cd17..64cdbdcf07 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnList.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnList.php @@ -20,6 +20,7 @@ class ColumnList extends Model 'type' => [ Response::MODEL_COLUMN_BOOLEAN, Response::MODEL_COLUMN_INTEGER, + Response::MODEL_COLUMN_BIGINT, Response::MODEL_COLUMN_FLOAT, Response::MODEL_COLUMN_EMAIL, Response::MODEL_COLUMN_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/Table.php b/src/Appwrite/Utopia/Response/Model/Table.php index 20cd3ccca2..b08bc2e97c 100644 --- a/src/Appwrite/Utopia/Response/Model/Table.php +++ b/src/Appwrite/Utopia/Response/Model/Table.php @@ -64,6 +64,7 @@ class Table extends Model 'type' => [ Response::MODEL_COLUMN_BOOLEAN, Response::MODEL_COLUMN_INTEGER, + Response::MODEL_COLUMN_BIGINT, Response::MODEL_COLUMN_FLOAT, Response::MODEL_COLUMN_EMAIL, Response::MODEL_COLUMN_ENUM, From 89eb5c2254d44dab8106e09a2f2c9197e28f9362 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 17:14:38 +0530 Subject: [PATCH 06/73] Refactor response model attribute and column types to prioritize BigInt over Integer for more accurate type matching in AttributeList, Collection, ColumnList, and Table classes. --- src/Appwrite/Utopia/Response/Model/AttributeList.php | 4 +++- src/Appwrite/Utopia/Response/Model/Collection.php | 4 +++- src/Appwrite/Utopia/Response/Model/ColumnList.php | 4 +++- src/Appwrite/Utopia/Response/Model/Table.php | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 70459c3df1..87d1dc8b9f 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -19,8 +19,10 @@ class AttributeList extends Model ->addRule('attributes', [ 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, - Response::MODEL_ATTRIBUTE_INTEGER, + // 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, Response::MODEL_ATTRIBUTE_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 7fe335cf09..bc4de22858 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -62,8 +62,10 @@ class Collection extends Model ->addRule('attributes', [ 'type' => [ Response::MODEL_ATTRIBUTE_BOOLEAN, - Response::MODEL_ATTRIBUTE_INTEGER, + // 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, Response::MODEL_ATTRIBUTE_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/ColumnList.php b/src/Appwrite/Utopia/Response/Model/ColumnList.php index 64cdbdcf07..0586015e4d 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnList.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnList.php @@ -19,8 +19,10 @@ class ColumnList extends Model ->addRule('columns', [ 'type' => [ Response::MODEL_COLUMN_BOOLEAN, - Response::MODEL_COLUMN_INTEGER, + // 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, Response::MODEL_COLUMN_ENUM, diff --git a/src/Appwrite/Utopia/Response/Model/Table.php b/src/Appwrite/Utopia/Response/Model/Table.php index b08bc2e97c..f9f2804fe5 100644 --- a/src/Appwrite/Utopia/Response/Model/Table.php +++ b/src/Appwrite/Utopia/Response/Model/Table.php @@ -63,8 +63,10 @@ class Table extends Model ->addRule('columns', [ 'type' => [ Response::MODEL_COLUMN_BOOLEAN, - Response::MODEL_COLUMN_INTEGER, + // 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, Response::MODEL_COLUMN_ENUM, From 5741041f33a543081e0654e6cd6a3682da18e842 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 17:32:49 +0530 Subject: [PATCH 07/73] updated tests --- tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php index d341681a65..66f08e6000 100644 --- a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -283,7 +283,7 @@ class DatabasesNumericTypesTest extends Scope // Update integer column $updateInteger = $this->client->call( Client::METHOD_PATCH, - '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer/integer_field', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -315,7 +315,7 @@ class DatabasesNumericTypesTest extends Scope // Update bigint column $updateBigint = $this->client->call( Client::METHOD_PATCH, - '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', + '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint/bigint_field', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 19487cf56b353cc797a088e9368818bf25a64ad0 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k <83803257+ArnabChatterjee20k@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:34:36 +0530 Subject: [PATCH 08/73] Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../Http/Databases/Collections/Attributes/BigInt/Create.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index f99ee38d91..6991b8ac16 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -58,9 +58,10 @@ class Create extends Action model: $this->getResponseModel(), ) ], + deprecated: new Deprecated( deprecated: new Deprecated( since: '1.8.0', - replaceWith: 'tablesDB.createIntegerColumn', + replaceWith: 'tablesDB.createBigIntColumn', ), )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) From e5d818ba44294c14124d5961032c586e31423b1f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 17:38:24 +0530 Subject: [PATCH 09/73] updated --- .../Databases/Collections/Attributes/BigInt/Create.php | 2 +- .../Databases/Collections/Attributes/BigInt/Update.php | 2 +- tests/e2e/Services/Migrations/MigrationsBase.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index f99ee38d91..1add0a6015 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -60,7 +60,7 @@ class Create extends Action ], deprecated: new Deprecated( since: '1.8.0', - replaceWith: 'tablesDB.createIntegerColumn', + replaceWith: 'tablesDB.createBigIntColumn', ), )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php index 8e4a060ace..4c795527cb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php @@ -58,7 +58,7 @@ class Update extends Action contentType: ContentType::JSON, deprecated: new Deprecated( since: '1.8.0', - replaceWith: 'tablesDB.updateIntegerColumn', + replaceWith: 'tablesDB.updateBigIntColumn', ), )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 0dc62a3be9..fef0a6d577 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1578,8 +1578,8 @@ trait MigrationsBase 'x-appwrite-key' => $this->getProject()['apiKey'] ], [ 'key' => 'bigint', - 'min' => 0, - 'max' => 100000, + 'min' => 2147483648, + 'max' => 9223372036854775807, 'required' => false, ]); @@ -1635,7 +1635,7 @@ trait MigrationsBase 'mediumtext' => 'mediumText', 'longtext' => 'longText', 'varchar' => 'varchar', - 'bigint' => $i * 1000, + 'bigint' => 2147483648 + $i, ] ]); @@ -1725,7 +1725,7 @@ trait MigrationsBase $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('1000', $csvData, 'CSV should contain bigint test data'); + $this->assertStringContainsString('2147483649', $csvData, 'CSV should contain bigint test data'); // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ From 819863559ef83697c14fa545de4e8535cb16358d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 17:45:25 +0530 Subject: [PATCH 10/73] Enhance BigInt support across database attributes and validation, updating types and tests for consistency. --- .../Http/Databases/Collections/Attributes/Action.php | 8 +++++++- .../Databases/Collections/Attributes/BigInt/Create.php | 5 ++--- .../Databases/Collections/Attributes/BigInt/Update.php | 2 +- src/Appwrite/Utopia/Response/Model/AttributeBigInt.php | 2 +- src/Appwrite/Utopia/Response/Model/ColumnBigInt.php | 2 +- tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php | 5 ++--- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 0d562a2894..e4c02b40ce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -237,6 +237,10 @@ abstract class Action extends UtopiaAction ? UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN : UtopiaResponse::MODEL_COLUMN_BOOLEAN, + Database::VAR_BIGINT => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT + : UtopiaResponse::MODEL_COLUMN_BIGINT, + Database::VAR_INTEGER => $isCollections ? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER : UtopiaResponse::MODEL_COLUMN_INTEGER, @@ -549,7 +553,9 @@ abstract class Action extends UtopiaAction } if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) { - $validator = new Range($min, $max, Database::VAR_INTEGER); + // Use bigint validator when updating a bigint attribute/column. + $rangeType = $type === Database::VAR_BIGINT ? Database::VAR_BIGINT : Database::VAR_INTEGER; + $validator = new Range($min, $max, $rangeType); } else { $validator = new Range($min, $max, Database::VAR_FLOAT); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index 6991b8ac16..a7df055094 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -58,7 +58,6 @@ class Create extends Action model: $this->getResponseModel(), ) ], - deprecated: new Deprecated( deprecated: new Deprecated( since: '1.8.0', replaceWith: 'tablesDB.createBigIntColumn', @@ -89,7 +88,7 @@ class Create extends Action throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value'); } - $validator = new Range($min, $max, Database::VAR_INTEGER); + $validator = new Range($min, $max, Database::VAR_BIGINT); if (!\is_null($default) && !$validator->isValid($default)) { throw new Exception($this->getInvalidValueException(), $validator->getDescription()); } @@ -99,7 +98,7 @@ class Create extends Action $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, - 'type' => Database::VAR_INTEGER, + 'type' => Database::VAR_BIGINT, 'size' => $size, 'required' => $required, 'default' => $default, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php index 4c795527cb..5d8e8bf3a5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php @@ -85,7 +85,7 @@ class Update extends Action dbForProject: $dbForProject, queueForEvents: $queueForEvents, authorization: $authorization, - type: Database::VAR_INTEGER, + type: Database::VAR_BIGINT, default: $default, required: $required, min: $min, diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php index 9e0d918fe0..2ab02991cb 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -7,7 +7,7 @@ use Appwrite\Utopia\Response; class AttributeBigInt extends AttributeInteger { public array $conditions = [ - 'type' => self::TYPE_INTEGER, + 'type' => 'bigint', 'size' => 8, ]; diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php index 9d7649f144..0d8b71ee98 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -7,7 +7,7 @@ use Appwrite\Utopia\Response; class ColumnBigInt extends ColumnInteger { public array $conditions = [ - 'type' => self::TYPE_INTEGER, + 'type' => 'bigint', 'size' => 8, ]; diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php index 66f08e6000..614a8f1f34 100644 --- a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -210,8 +210,7 @@ class DatabasesNumericTypesTest extends Scope $this->assertEquals(200, $bigintColumn['headers']['status-code']); $this->assertEquals('bigint_field', $bigintColumn['body']['key']); - // Some implementations may still represent bigint columns as integer internally. - $this->assertTrue(\in_array($bigintColumn['body']['type'], ['bigint', 'integer'], true)); + $this->assertEquals('bigint', $bigintColumn['body']['type']); $this->assertEquals(false, $bigintColumn['body']['required']); $this->assertEquals(false, $bigintColumn['body']['array']); $this->assertEquals(-900719925, $bigintColumn['body']['min']); @@ -245,7 +244,7 @@ class DatabasesNumericTypesTest extends Scope } $this->assertEquals('integer', $columnTypeByKey['integer_field']); - $this->assertTrue(\in_array($columnTypeByKey['bigint_field'], ['bigint', 'integer'], true)); + $this->assertEquals('bigint', $columnTypeByKey['bigint_field']); } public function testCreateRowWithIntegerAndBigIntTypes(): void From 737bfc599a7b7c467e9798b768ac4b1f4b4f1b98 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 18:00:43 +0530 Subject: [PATCH 11/73] fixed analyze --- tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php index 614a8f1f34..2da7769672 100644 --- a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -29,8 +29,8 @@ class DatabasesNumericTypesTest extends Scope protected function setupDatabaseAndTable(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$setupCache[$cacheKey])) { - return static::$setupCache[$cacheKey]; + if (!empty(self::$setupCache[$cacheKey])) { + return self::$setupCache[$cacheKey]; } $projectId = $this->getProject()['$id']; @@ -82,7 +82,7 @@ class DatabasesNumericTypesTest extends Scope // Cache before waiting so that if waitForAllAttributes times out, // subsequent calls don't try to re-create the same columns (causing 409) - static::$setupCache[$cacheKey] = [ + self::$setupCache[$cacheKey] = [ 'databaseId' => $databaseId, 'tableId' => $tableId, ]; @@ -90,7 +90,7 @@ class DatabasesNumericTypesTest extends Scope // Wait for all columns to be available $this->waitForAllAttributes($databaseId, $tableId); - return static::$setupCache[$cacheKey]; + return self::$setupCache[$cacheKey]; } /** From a5b2021d1e3eef44f32930239b81536a9d2f5189 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 18:17:14 +0530 Subject: [PATCH 12/73] empty From dfbf22cf3cf17f46e3f53d5f36dddfd19f3b5ec6 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 18:36:19 +0530 Subject: [PATCH 13/73] updated format --- app/init/database/formats.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/init/database/formats.php b/app/init/database/formats.php index 29a4f0c7d4..a4d80bea3e 100644 --- a/app/init/database/formats.php +++ b/app/init/database/formats.php @@ -36,6 +36,13 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { return new Range($min, $max, Range::TYPE_INTEGER); }, Database::VAR_INTEGER); +// BigInt uses the same intRange format, but is stored as a 64-bit integer type. +Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { + $min = $attribute['formatOptions']['min'] ?? -INF; + $max = $attribute['formatOptions']['max'] ?? INF; + return new Range($min, $max, Range::TYPE_INTEGER); +}, Database::VAR_BIGINT); + Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { $min = $attribute['formatOptions']['min'] ?? -INF; $max = $attribute['formatOptions']['max'] ?? INF; From 2fb54e084628468517826203d4b58a7e0593fe75 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 18:59:24 +0530 Subject: [PATCH 14/73] Add BigInt support with dedicated bigintRange format and update related logic --- app/init/constants.php | 1 + app/init/database/formats.php | 4 ++-- .../Databases/Collections/Attributes/Action.php | 14 +++++++++----- .../Collections/Attributes/BigInt/Create.php | 2 +- .../Http/Databases/Collections/Create.php | 14 +++++++++----- tests/e2e/Services/Migrations/MigrationsBase.php | 1 - 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index ab88be5854..2aa9a938ab 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -54,6 +54,7 @@ const APP_DATABASE_ATTRIBUTE_IP = 'ip'; const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime'; const APP_DATABASE_ATTRIBUTE_URL = 'url'; const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange'; +const APP_DATABASE_ATTRIBUTE_BIGINT_RANGE = 'bigintRange'; const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange'; const APP_DATABASE_ATTRIBUTE_POINT = 'point'; const APP_DATABASE_ATTRIBUTE_LINE = 'line'; diff --git a/app/init/database/formats.php b/app/init/database/formats.php index a4d80bea3e..9ecf07716a 100644 --- a/app/init/database/formats.php +++ b/app/init/database/formats.php @@ -36,8 +36,8 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { return new Range($min, $max, Range::TYPE_INTEGER); }, Database::VAR_INTEGER); -// BigInt uses the same intRange format, but is stored as a 64-bit integer type. -Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) { +// 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); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index e4c02b40ce..0825f6a48e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -544,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']; @@ -552,16 +553,19 @@ 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) { - // Use bigint validator when updating a bigint attribute/column. - $rangeType = $type === Database::VAR_BIGINT ? Database::VAR_BIGINT : Database::VAR_INTEGER; - $validator = new Range($min, $max, $rangeType); - } 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 + // but validate against different primitive types. + $rangeType = $attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_BIGINT_RANGE + ? Database::VAR_BIGINT + : Database::VAR_INTEGER; + $validator = new Range($min, $max, $rangeType); } if (!is_null($default) && !$validator->isValid($default)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index a7df055094..74e63e2f2d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -103,7 +103,7 @@ class Create extends Action 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, + 'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index fd309a413c..788222d4be 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -290,13 +290,17 @@ 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; + if ($type === Database::VAR_INTEGER) { + $format = APP_DATABASE_ATTRIBUTE_INT_RANGE; + } elseif ($type === Database::VAR_BIGINT) { + $format = APP_DATABASE_ATTRIBUTE_BIGINT_RANGE; + } else { + $format = APP_DATABASE_ATTRIBUTE_FLOAT_RANGE; + } $formatOptions = [ - 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX), - 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX), + 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX), + 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX), ]; } diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index fef0a6d577..41710252e4 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1256,7 +1256,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'); From 5fa4551400f930d1a42d25e45da559c2acceb653 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 20:10:13 +0530 Subject: [PATCH 15/73] Refactor bigint handling in validation and action classes to unify range validation logic --- composer.lock | 8 ++--- .../Collections/Attributes/Action.php | 10 +----- .../Collections/Attributes/BigInt/Create.php | 2 +- .../Utopia/Database/Validator/Attributes.php | 34 ++++++++++++++----- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index 036415e843..cdc650af86 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ef5c77972a95129eb929cce6c8f7c674db930ec9" + "reference": "b87cf3a9727f24cf080d8d2c21f376b88b308668" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ef5c77972a95129eb929cce6c8f7c674db930ec9", - "reference": "ef5c77972a95129eb929cce6c8f7c674db930ec9", + "url": "https://api.github.com/repos/utopia-php/database/zipball/b87cf3a9727f24cf080d8d2c21f376b88b308668", + "reference": "b87cf3a9727f24cf080d8d2c21f376b88b308668", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-03-30T06:31:05+00:00" + "time": "2026-03-30T14:36:08+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 0825f6a48e..d726d613be 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -237,10 +237,6 @@ abstract class Action extends UtopiaAction ? UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN : UtopiaResponse::MODEL_COLUMN_BOOLEAN, - Database::VAR_BIGINT => $isCollections - ? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT - : UtopiaResponse::MODEL_COLUMN_BIGINT, - Database::VAR_INTEGER => $isCollections ? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER : UtopiaResponse::MODEL_COLUMN_INTEGER, @@ -561,11 +557,7 @@ abstract class Action extends UtopiaAction } } else { // intRange and bigintRange share the same integer range semantics - // but validate against different primitive types. - $rangeType = $attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_BIGINT_RANGE - ? Database::VAR_BIGINT - : Database::VAR_INTEGER; - $validator = new Range($min, $max, $rangeType); + $validator = new Range($min, $max, Range::TYPE_INTEGER); } if (!is_null($default) && !$validator->isValid($default)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index 74e63e2f2d..c0544fb122 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -88,7 +88,7 @@ class Create extends Action throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value'); } - $validator = new Range($min, $max, Database::VAR_BIGINT); + $validator = new Range($min, $max, Range::TYPE_INTEGER); if (!\is_null($default) && !$validator->isValid($default)) { throw new Exception($this->getInvalidValueException(), $validator->getDescription()); } diff --git a/src/Appwrite/Utopia/Database/Validator/Attributes.php b/src/Appwrite/Utopia/Database/Validator/Attributes.php index f8bdd01103..34562a2536 100644 --- a/src/Appwrite/Utopia/Database/Validator/Attributes.php +++ b/src/Appwrite/Utopia/Database/Validator/Attributes.php @@ -23,6 +23,7 @@ class Attributes extends Validator protected array $supportedTypes = [ Database::VAR_STRING, Database::VAR_INTEGER, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, @@ -181,9 +182,9 @@ class Attributes extends Validator return false; } - // Validate signed only for integer/float types - if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) { - $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types"; + // Validate signed only for integer/bigint/float types + if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) { + $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer, bigint or float types"; return false; } @@ -199,10 +200,10 @@ class Attributes extends Validator return false; } - // Validate min/max range for integer/float + // Validate min/max range for integer/bigint/float if (isset($attribute['min']) || isset($attribute['max'])) { - if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) { - $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types"; + if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) { + $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer, bigint or float types"; return false; } @@ -264,7 +265,7 @@ class Attributes extends Validator if (isset($attribute['min']) || isset($attribute['max'])) { $min = $attribute['min'] ?? \PHP_INT_MIN; $max = $attribute['max'] ?? \PHP_INT_MAX; - $rangeValidator = new Range($min, $max, Database::VAR_INTEGER); + $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER); if (!$rangeValidator->isValid($attribute['default'])) { $this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max"; return false; @@ -272,6 +273,23 @@ class Attributes extends Validator } break; + case Database::VAR_BIGINT: + if (!is_int($attribute['default'])) { + $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be an integer"; + return false; + } + // Validate within range if min/max specified + if (isset($attribute['min']) || isset($attribute['max'])) { + $min = $attribute['min'] ?? \PHP_INT_MIN; + $max = $attribute['max'] ?? \PHP_INT_MAX; + $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER); + if (!$rangeValidator->isValid($attribute['default'])) { + $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be between $min and $max"; + return false; + } + } + break; + case Database::VAR_FLOAT: if (!is_float($attribute['default']) && !is_int($attribute['default'])) { $this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number"; @@ -281,7 +299,7 @@ class Attributes extends Validator if (isset($attribute['min']) || isset($attribute['max'])) { $min = $attribute['min'] ?? -\PHP_FLOAT_MAX; $max = $attribute['max'] ?? \PHP_FLOAT_MAX; - $rangeValidator = new Range($min, $max, Database::VAR_FLOAT); + $rangeValidator = new Range($min, $max, Range::TYPE_FLOAT); if (!$rangeValidator->isValid((float)$attribute['default'])) { $this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max"; return false; From c4b3956beff6a28c49c90a37a7b5ab3da53ed9ab Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 20:26:23 +0530 Subject: [PATCH 16/73] emtpy From ad7f83db11b14203583a77867153e2634c3f5b83 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 20:32:03 +0530 Subject: [PATCH 17/73] updated actions --- .../Http/Databases/Collections/Attributes/Action.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index d726d613be..3b28bdeffb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -241,6 +241,10 @@ abstract class Action extends UtopiaAction ? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER : UtopiaResponse::MODEL_COLUMN_INTEGER, + Database::VAR_BIGINT => $isCollections + ? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT + : UtopiaResponse::MODEL_COLUMN_BIGINT, + Database::VAR_FLOAT => $isCollections ? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT : UtopiaResponse::MODEL_COLUMN_FLOAT, From 71b28eae5a7f0bcb44d9ee1e9d80353eb8ab909f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 1 Apr 2026 15:55:26 +0530 Subject: [PATCH 18/73] updated db --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 59173e9794..869bee8d7d 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "b87cf3a9727f24cf080d8d2c21f376b88b308668" + "reference": "ad89dab91d0e85f4496d366ad80b687cbdefd714" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/b87cf3a9727f24cf080d8d2c21f376b88b308668", - "reference": "b87cf3a9727f24cf080d8d2c21f376b88b308668", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ad89dab91d0e85f4496d366ad80b687cbdefd714", + "reference": "ad89dab91d0e85f4496d366ad80b687cbdefd714", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-03-30T14:36:08+00:00" + "time": "2026-04-01T05:25:04+00:00" }, { "name": "utopia-php/detector", From 45dc259df3d341be3001b0ec8d264742c65f819b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 11:14:28 +0530 Subject: [PATCH 19/73] updated validator --- composer.lock | 8 ++++---- .../Http/TablesDB/Tables/Columns/BigInt/Create.php | 3 ++- .../Http/TablesDB/Tables/Columns/BigInt/Update.php | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 869bee8d7d..a1fa0b153a 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ad89dab91d0e85f4496d366ad80b687cbdefd714" + "reference": "17615116013c18b8f4d00246d52268ffbc15b11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ad89dab91d0e85f4496d366ad80b687cbdefd714", - "reference": "ad89dab91d0e85f4496d366ad80b687cbdefd714", + "url": "https://api.github.com/repos/utopia-php/database/zipball/17615116013c18b8f4d00246d52268ffbc15b11c", + "reference": "17615116013c18b8f4d00246d52268ffbc15b11c", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-04-01T05:25:04+00:00" + "time": "2026-04-09T05:22:46+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php index 1b7b33291b..2b5dd078b4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -8,6 +8,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\BigInt; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; @@ -58,7 +59,7 @@ class Create extends BigIntCreate ->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('default', null, new Nullable(new BigInt(false, true)), 'Default value. Cannot be set when column is required.', true) ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php index 387dd08238..bcec75f713 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Validator\BigInt; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; @@ -60,7 +61,7 @@ class Update extends BigIntUpdate ->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('default', null, new Nullable(new BigInt(false, true)), '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') From 0c734f8f1205ae6344382e791b1b303d5781e29f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 12:02:13 +0530 Subject: [PATCH 20/73] updated --- composer.lock | 36 +++++++++---------- .../SDK/Specification/Format/OpenAPI3.php | 9 +++++ .../SDK/Specification/Format/Swagger2.php | 9 +++++ 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index 45ced3bce8..141ef45c01 100644 --- a/composer.lock +++ b/composer.lock @@ -3403,16 +3403,16 @@ }, { "name": "utopia-php/agents", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/agents.git", - "reference": "052227953678a30ecc4b5467401fcb0b2386471e" + "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e", - "reference": "052227953678a30ecc4b5467401fcb0b2386471e", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30", + "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30", "shasum": "" }, "require": { @@ -3450,9 +3450,9 @@ ], "support": { "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/1.2.1" + "source": "https://github.com/utopia-php/agents/tree/1.3.0" }, - "time": "2026-02-24T06:03:55+00:00" + "time": "2026-03-26T03:51:11+00:00" }, { "name": "utopia-php/analytics", @@ -5225,16 +5225,16 @@ }, { "name": "utopia-php/vcs", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af" + "reference": "44a84ab52b42fc12f812b4d7331286b519d39db3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af", - "reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3", + "reference": "44a84ab52b42fc12f812b4d7331286b519d39db3", "shasum": "" }, "require": { @@ -5268,9 +5268,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/3.1.0" + "source": "https://github.com/utopia-php/vcs/tree/3.2.0" }, - "time": "2026-03-24T08:49:14+00:00" + "time": "2026-04-08T16:00:31+00:00" }, { "name": "utopia-php/websocket", @@ -5448,16 +5448,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.17.7", + "version": "1.17.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e" + "reference": "b7109e13cec89ed56ad80111973b12646e4eb5f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e", - "reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b7109e13cec89ed56ad80111973b12646e4eb5f7", + "reference": "b7109e13cec89ed56ad80111973b12646e4eb5f7", "shasum": "" }, "require": { @@ -5493,9 +5493,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.17.7" + "source": "https://github.com/appwrite/sdk-generator/tree/1.17.8" }, - "time": "2026-04-08T08:51:05+00:00" + "time": "2026-04-09T05:08:21+00:00" }, { "name": "brianium/paratest", diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 94e0f831b7..0f3b2568a2 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -431,6 +431,15 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; + case \Utopia\Database\Validator\BigInt::class: + // BigInt validator reports Database::VAR_BIGINT, but OpenAPI expects scalar types. + // We expose it as int64 to keep schema consistent with Column/Attribute models. + $node['schema']['type'] = 'integer'; + $node['schema']['format'] = 'int64'; + if (!empty($param['example'])) { + $node['schema']['x-example'] = $param['example']; + } + break; case \Utopia\Validator\Boolean::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: false; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 14a18eea2e..f47ca03152 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -435,6 +435,15 @@ class Swagger2 extends Format $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; + case \Utopia\Database\Validator\BigInt::class: + // BigInt validator reports Database::VAR_BIGINT, but Swagger expects scalar types. + // We expose it as int64 to keep schema consistent with Column/Attribute models. + $node['type'] = 'integer'; + $node['format'] = 'int64'; + if (!empty($param['example'])) { + $node['x-example'] = $param['example']; + } + break; case \Utopia\Validator\Boolean::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: false; From 0ea83790651e33784aa083ffb5134bb2c782e54c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 14:11:21 +0530 Subject: [PATCH 21/73] added the changes --- .../Http/TablesDB/Tables/Columns/BigInt/Create.php | 3 +-- .../Http/TablesDB/Tables/Columns/BigInt/Update.php | 3 +-- src/Appwrite/SDK/Specification/Format/Swagger2.php | 9 --------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php index 2b5dd078b4..1b7b33291b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -8,7 +8,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; -use Utopia\Database\Validator\BigInt; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; @@ -59,7 +58,7 @@ class Create extends BigIntCreate ->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 BigInt(false, true)), 'Default value. Cannot be set when column is required.', 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') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php index bcec75f713..387dd08238 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -9,7 +9,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; -use Utopia\Database\Validator\BigInt; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; @@ -61,7 +60,7 @@ class Update extends BigIntUpdate ->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 BigInt(false, true)), 'Default value. Cannot be set when column is required.') + ->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') diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index f47ca03152..14a18eea2e 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -435,15 +435,6 @@ class Swagger2 extends Format $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case \Utopia\Database\Validator\BigInt::class: - // BigInt validator reports Database::VAR_BIGINT, but Swagger expects scalar types. - // We expose it as int64 to keep schema consistent with Column/Attribute models. - $node['type'] = 'integer'; - $node['format'] = 'int64'; - if (!empty($param['example'])) { - $node['x-example'] = $param['example']; - } - break; case \Utopia\Validator\Boolean::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: false; From 4f599fc75cb5e8cd75b48b0c9a93ad772fff18e4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 17:07:47 +0530 Subject: [PATCH 22/73] updated --- composer.lock | 8 +-- .../Collections/Attributes/BigInt/Create.php | 5 +- .../Http/Databases/Collections/Create.php | 11 +++- .../Utopia/Response/Model/AttributeBigInt.php | 51 ++++++++++++++++--- .../Utopia/Response/Model/ColumnBigInt.php | 51 ++++++++++++++++--- 5 files changed, 101 insertions(+), 25 deletions(-) diff --git a/composer.lock b/composer.lock index 141ef45c01..bec82a1a4c 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "17615116013c18b8f4d00246d52268ffbc15b11c" + "reference": "0e43fd44f2190da36cac816a17650d697e034507" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/17615116013c18b8f4d00246d52268ffbc15b11c", - "reference": "17615116013c18b8f4d00246d52268ffbc15b11c", + "url": "https://api.github.com/repos/utopia-php/database/zipball/0e43fd44f2190da36cac816a17650d697e034507", + "reference": "0e43fd44f2190da36cac816a17650d697e034507", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-04-09T05:22:46+00:00" + "time": "2026-04-09T11:05:50+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index c0544fb122..4ea85b71e6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -93,13 +93,10 @@ class Create extends Action throw new Exception($this->getInvalidValueException(), $validator->getDescription()); } - // `bigint` is always stored as a 64-bit integer. - $size = 8; - $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_BIGINT, - 'size' => $size, + 'size' => 8, 'required' => $required, 'default' => $default, 'array' => $array, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index 788222d4be..24cec35eac 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -276,7 +276,16 @@ class Create extends Action ): array { $key = $attribute['key']; $type = $attribute['type']; - $size = $attribute['size'] ?? 0; + switch ($type) { + case Database::VAR_INTEGER: + $size = 4; + break; + case Database::VAR_BIGINT: + $size = 8; + break; + default: + $size = $attribute['size'] ?? 0; + } $required = $attribute['required'] ?? false; $signed = $attribute['signed'] ?? true; $array = $attribute['array'] ?? false; diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php index 2ab02991cb..722bcedf5a 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -4,21 +4,56 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; -class AttributeBigInt extends AttributeInteger +class AttributeBigInt extends Attribute { - public array $conditions = [ - 'type' => 'bigint', - 'size' => 8, - ]; - public function __construct() { parent::__construct(); - // Update example for the `type` field - $this->rules['type']['example'] = 'bigint'; + $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, + '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', + 'size' => 8, + ]; + public function getName(): string { return 'AttributeBigInt'; diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php index 0d8b71ee98..c4d7f96586 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -4,21 +4,56 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; -class ColumnBigInt extends ColumnInteger +class ColumnBigInt extends Column { - public array $conditions = [ - 'type' => 'bigint', - 'size' => 8, - ]; - public function __construct() { parent::__construct(); - // Update example for the `type` field - $this->rules['type']['example'] = 'bigint'; + $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, + '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', + 'size' => 8, + ]; + public function getName(): string { return 'ColumnBigInt'; From b3c6e0e67f19f9c5932bf3e90340e9d35c6a9902 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 17:10:47 +0530 Subject: [PATCH 23/73] removed size from the conditions --- src/Appwrite/Utopia/Response/Model/AttributeBigInt.php | 3 +-- src/Appwrite/Utopia/Response/Model/ColumnBigInt.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php index 722bcedf5a..217589c578 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -50,8 +50,7 @@ class AttributeBigInt extends Attribute } public array $conditions = [ - 'type' => 'bigint', - 'size' => 8, + 'type' => 'bigint' ]; public function getName(): string diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php index c4d7f96586..e001b6aedf 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -50,8 +50,7 @@ class ColumnBigInt extends Column } public array $conditions = [ - 'type' => 'bigint', - 'size' => 8, + 'type' => 'bigint' ]; public function getName(): string From d84f71e728fe9af0b412cf325daafd05d9ed5431 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 16 Apr 2026 14:10:20 +0530 Subject: [PATCH 24/73] updated --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 270491a291..d456c314a3 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "7315ba260141830ec1f51963955e12695988c281" + "reference": "6ff2d7b081f99280ffb85dd6efa710c9aeb3e620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/7315ba260141830ec1f51963955e12695988c281", - "reference": "7315ba260141830ec1f51963955e12695988c281", + "url": "https://api.github.com/repos/utopia-php/database/zipball/6ff2d7b081f99280ffb85dd6efa710c9aeb3e620", + "reference": "6ff2d7b081f99280ffb85dd6efa710c9aeb3e620", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-04-16T08:36:02+00:00" + "time": "2026-04-16T08:39:46+00:00" }, { "name": "utopia-php/detector", From e7072574adb5a6035bc207f232bd2141de453ca6 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 16 Apr 2026 14:13:01 +0530 Subject: [PATCH 25/73] updated tests --- .../Http/Databases/Collections/Create.php | 3 --- .../TablesDB/DatabasesNumericTypesTest.php | 18 +++++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index 24cec35eac..32c311b10a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -277,9 +277,6 @@ class Create extends Action $key = $attribute['key']; $type = $attribute['type']; switch ($type) { - case Database::VAR_INTEGER: - $size = 4; - break; case Database::VAR_BIGINT: $size = 8; break; diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php index 2da7769672..0e46d9466c 100644 --- a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -75,9 +75,9 @@ class DatabasesNumericTypesTest extends Scope $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ 'key' => 'bigint_field', 'required' => false, - 'min' => -900719925, - 'max' => 900719925, - 'default' => 123, + 'min' => -9007199254740991, + 'max' => 9007199254740991, + 'default' => 9007199254740000, ]); // Cache before waiting so that if waitForAllAttributes times out, @@ -139,9 +139,9 @@ class DatabasesNumericTypesTest extends Scope $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [ 'key' => 'bigint_field', 'required' => false, - 'min' => -900719925, - 'max' => 900719925, - 'default' => 123, + 'min' => -9007199254740991, + 'max' => 9007199254740991, + 'default' => 9007199254740000, ]); $this->waitForAllAttributes($databaseId, $tableId); @@ -213,9 +213,9 @@ class DatabasesNumericTypesTest extends Scope $this->assertEquals('bigint', $bigintColumn['body']['type']); $this->assertEquals(false, $bigintColumn['body']['required']); $this->assertEquals(false, $bigintColumn['body']['array']); - $this->assertEquals(-900719925, $bigintColumn['body']['min']); - $this->assertEquals(900719925, $bigintColumn['body']['max']); - $this->assertEquals(123, $bigintColumn['body']['default']); + $this->assertEquals(-9007199254740991, $bigintColumn['body']['min']); + $this->assertEquals(9007199254740991, $bigintColumn['body']['max']); + $this->assertEquals(9007199254740000, $bigintColumn['body']['default']); } public function testListColumnsWithNumericTypes(): void From 9c9bde2ce689a0bfec68e4b69323a0f66b7d2a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 11:36:54 +0200 Subject: [PATCH 26/73] Introduce project key console tests --- .env | 2 +- tests/e2e/Services/Proxy/ProxyBase.php | 923 +++++++++++++----- .../Services/Proxy/ProxyConsoleClientTest.php | 14 + .../Services/Proxy/ProxyCustomServerTest.php | 745 -------------- tests/e2e/Services/Proxy/ProxyHelpers.php | 293 ++++++ 5 files changed, 997 insertions(+), 980 deletions(-) create mode 100644 tests/e2e/Services/Proxy/ProxyConsoleClientTest.php create mode 100644 tests/e2e/Services/Proxy/ProxyHelpers.php diff --git a/.env b/.env index 4a6a3ac344..d57e7cf421 100644 --- a/.env +++ b/.env @@ -20,7 +20,7 @@ _APP_EMAIL_CERTIFICATES=certificates@appwrite.io _APP_SYSTEM_RESPONSE_FORMAT= _APP_CUSTOM_DOMAIN_DENY_LIST= _APP_OPTIONS_ABUSE=disabled -_APP_OPTIONS_ROUTER_PROTECTION=disabled +_APP_OPTIONS_ROUTER_PROTECTION=enabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 59a853bfc8..33ef64591b 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -2,298 +2,753 @@ namespace Tests\E2E\Services\Proxy; -use Appwrite\ID; -use Appwrite\Tests\Async; -use CURLFile; use Tests\E2E\Client; -use Utopia\Console; +use Utopia\Database\Query; +use Utopia\System\System; trait ProxyBase { - use Async; + use ProxyHelpers; - protected function listRules(array $params = []): mixed + protected function tearDown(): void { - $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $rule; - } - - protected function createAPIRule(string $domain): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, + // Cleanup for testRuleVerification test + // Required as it uses static domain name + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1000)->toString(), + ] ]); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $ruleId = $rule['$id']; + $response = $this->deleteRule($ruleId); + $this->assertEquals(204, $response['headers']['status-code']); + } - return $rule; + if ($rules['body']['total'] > 0) { + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, count($rules['body']['rules'])); + $this->assertEquals(0, $rules['body']['total']); + } } - protected function updateRuleVerification(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'siteId' => $siteId, - 'branch' => $branch, - ]); - - return $rule; - } - - protected function getRule(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'url' => $url, - 'statusCode' => $statusCode, - 'resourceType' => $resourceType, - 'resourceId' => $resourceId, - ]); - - return $rule; - } - - protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed - { - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'domain' => $domain, - 'functionId' => $functionId, - 'branch' => $branch, - ]); - - return $rule; - } - - protected function deleteRule(string $ruleId): mixed - { - $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $rule; - } - - protected function setupAPIRule(string $domain): string + public function testCreateRule(): void { + $domain = \uniqid() . '-api.myapp.com'; $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertEquals('manual', $rule['body']['trigger']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('domain', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('redirectUrl', $rule['body']); + $this->assertArrayHasKey('redirectStatusCode', $rule['body']); + $this->assertArrayHasKey('deploymentResourceType', $rule['body']); + $this->assertArrayHasKey('deploymentId', $rule['body']); + $this->assertArrayHasKey('deploymentResourceId', $rule['body']); + $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); - return $rule['body']['$id']; - } + $ruleId = $rule['body']['$id']; - protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string - { - $rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId); + $rule = $this->createAPIRule($domain); + $this->assertEquals(409, $rule['headers']['status-code']); - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string - { - $rule = $this->createFunctionRule($domain, $functionId, $branch); - - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string - { - $rule = $this->createSiteRule($domain, $siteId, $branch); - - $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); - - return $rule['body']['$id']; - } - - protected function cleanupRule(string $ruleId): void - { $rule = $this->deleteRule($ruleId); - $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule)); + + $this->assertEquals(204, $rule['headers']['status-code']); } - protected function cleanupSite(string $siteId): void + public function testCreateRuleSetup(): void { - $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site)); + $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); + $this->cleanupRule($ruleId); } - protected function cleanupFunction(string $functionId): void + public function testCreateRuleApex(): void { - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function)); + $domain = \uniqid() . '.com'; + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $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('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); } - 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('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']); } - 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->updateRuleVerification($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals($ruleId, $rule['body']['$id']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + $this->cleanupSite($siteId); + + // 2. Function rule can verify + $function = $this->setupFunction(); + $functionId = $function['functionId']; + + $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + $this->cleanupRule($rule['body']['$id']); + + $rule = $this->createAPIRule('stage-site.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); + $this->cleanupRule($rule['body']['$id']); + + $this->cleanupFunction($functionId); + + // 3. Wrong A record fails to verify + $rule = $this->createAPIRule('wrong-a-webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 4. Correct A record can verify + $rule = $this->createAPIRule('webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + + // 5. Correct CNAME record can verify (no CAA record) + $rule = $this->createAPIRule('stage.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + + // 6. Missing CNAME record fails to verify + $rule = $this->createAPIRule('stage-missing-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 7. Wrong CNAME record fails to verify + $rule = $this->createAPIRule('stage-wrong-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 8. Wrong CAA record fails to verify + $rule = $this->createAPIRule('stage-wrong-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 9. Correct CAA record can verify + $rule = $this->createAPIRule('stage-correct-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['logs']); + + $this->cleanupRule($rule['body']['$id']); + } + + public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void + { + $domain = \uniqid() . '-timestamp-test.webapp.com'; + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertNotEmpty($rule['body']['logs']); + + $ruleId = $rule['body']['$id']; + $initialUpdatedAt = $rule['body']['$updatedAt']; + $initiallogs = $rule['body']['logs']; + + sleep(1); + + $updatedRule = $this->updateRuleVerification($ruleId); + + $this->assertEquals(400, $updatedRule['headers']['status-code']); + $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']); + + $ruleAfterUpdate = $this->getRule($ruleId); + $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']); + $this->assertEquals('created', $ruleAfterUpdate['body']['status']); + $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']); + $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']); + + $initialTime = new \DateTime($initialUpdatedAt); + $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']); + $this->assertGreaterThan($initialTime, $updatedTime); + + $this->cleanupRule($ruleId); } } diff --git a/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php new file mode 100644 index 0000000000..68761f34a9 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php @@ -0,0 +1,14 @@ +listRules([ - 'queries' => [ - Query::endsWith('domain', 'webapp.com')->toString(), - Query::limit(1000)->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $ruleId = $rule['$id']; - $response = $this->deleteRule($ruleId); - $this->assertEquals(204, $response['headers']['status-code']); - } - - if ($rules['body']['total'] > 0) { - $rules = $this->listRules([ - 'queries' => [ - Query::endsWith('domain', 'webapp.com')->toString(), - Query::limit(1)->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, count($rules['body']['rules'])); - $this->assertEquals(0, $rules['body']['total']); - } - } - - public function testCreateRule(): void - { - $domain = \uniqid() . '-api.myapp.com'; - $rule = $this->createAPIRule($domain); - - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals($domain, $rule['body']['domain']); - $this->assertEquals('manual', $rule['body']['trigger']); - $this->assertArrayHasKey('$id', $rule['body']); - $this->assertArrayHasKey('domain', $rule['body']); - $this->assertArrayHasKey('type', $rule['body']); - $this->assertArrayHasKey('redirectUrl', $rule['body']); - $this->assertArrayHasKey('redirectStatusCode', $rule['body']); - $this->assertArrayHasKey('deploymentResourceType', $rule['body']); - $this->assertArrayHasKey('deploymentId', $rule['body']); - $this->assertArrayHasKey('deploymentResourceId', $rule['body']); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); - $this->assertArrayHasKey('logs', $rule['body']); - $this->assertArrayHasKey('renewAt', $rule['body']); - - $ruleId = $rule['body']['$id']; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(409, $rule['headers']['status-code']); - - $rule = $this->deleteRule($ruleId); - - $this->assertEquals(204, $rule['headers']['status-code']); - } - - public function testCreateRuleSetup(): void - { - $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); - $this->cleanupRule($ruleId); - } - - public function testCreateRuleApex(): void - { - $domain = \uniqid() . '.com'; - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - } - - public function testCreateRuleVcs(): void - { - $domain = \uniqid() . '-vcs.myapp.com'; - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $rule = $this->createSiteRule('commit-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createSiteRule('branch-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createSiteRule('anything-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - - $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; - $domain = \uniqid() . '-vcs.' . $sitesDomain; - - $rule = $this->createSiteRule('commit-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('branch-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createSiteRule('anything-' . $domain, $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->cleanupRule($rule['body']['$id']); - } - - public function testCreateAPIRule(): void - { - $domain = \uniqid() . '-api.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/versions'); - $this->assertEquals(401, $response['headers']['status-code']); - - $ruleId = $this->setupAPIRule($domain); - - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/versions'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); - - $this->cleanupRule($ruleId); - - $rule = $this->createAPIRule('http://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule('https://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule('wss://' . $domain); - $this->assertEquals(400, $rule['headers']['status-code']); - - $rule = $this->createAPIRule($domain . '/some-path'); - $this->assertEquals(400, $rule['headers']['status-code']); - } - - public function testCreateRedirectRule(): void - { - $domain = \uniqid() . '-redirect.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); - $this->assertEquals(401, $response['headers']['status-code']); - - $siteId = $this->setupSite()['siteId']; - - $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId); - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['id']); - - $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['id']); - - $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); - $this->assertEquals(301, $response['headers']['status-code']); - $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); - - $domain = \uniqid() . '-redirect-307.custom.localhost'; - $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId); - $this->assertNotEmpty($ruleId); - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); - $this->assertEquals(307, $response['headers']['status-code']); - $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('type', ['redirect'])->toString(), - Query::equal('trigger', ['manual'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ], - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - - $this->cleanupSite($siteId); - $this->cleanupRule($ruleId); - } - - public function testCreateFunctionRule(): void - { - $domain = \uniqid() . '-function.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/ping'); - $this->assertEquals(401, $response['headers']['status-code']); - - $setup = $this->setupFunction(); - $functionId = $setup['functionId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($functionId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupFunctionRule($domain, $functionId); - $this->assertNotEmpty($ruleId); - - $response = $proxyClient->call(Client::METHOD_GET, '/ping'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']); - - $this->cleanupRule($ruleId); - - $this->cleanupFunction($functionId); - - $this->assertEventually(function () use ($functionId, $deploymentId) { - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['function'])->toString(), - Query::equal('deploymentResourceId', [$functionId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentId', [$deploymentId])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - }); - } - - public function testCreateSiteRule(): void - { - $domain = \uniqid() . '-site.custom.localhost'; - - $proxyClient = new Client(); - $proxyClient->setEndpoint('http://appwrite.test'); - $proxyClient->addHeader('x-appwrite-hostname', $domain); - - $response = $proxyClient->call(Client::METHOD_GET, '/contact'); - $this->assertEquals(401, $response['headers']['status-code']); - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupSiteRule($domain, $siteId); - $this->assertNotEmpty($ruleId); - $rule = $this->getRule($ruleId); - $this->assertSame(200, $rule['headers']['status-code']); - $this->assertSame('created', $rule['body']['status']); - - $response = $proxyClient->call(Client::METHOD_GET, '/contact'); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertStringContainsString('Contact page', $response['body']); - - // Wildcard domains automatically get verified status - $domains = [ - \uniqid() . '.sites.localhost', - \uniqid() . '.rebranded.localhost', - ]; - foreach ($domains as $domain) { - $wildcardRuleId = $this->setupSiteRule($domain, $siteId); - $this->assertNotEmpty($wildcardRuleId); - $rule = $this->getRule($wildcardRuleId); - $this->assertSame(200, $rule['headers']['status-code']); - $this->assertSame('verified', $rule['body']['status']); - $this->cleanupRule($wildcardRuleId); - } - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('trigger', ['deployment'])->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertGreaterThan(0, $rules['body']['total']); - - $this->cleanupRule($ruleId); - - $this->cleanupSite($siteId); - - $this->assertEventually(function () use ($siteId, $deploymentId) { - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentResourceType', ['site'])->toString(), - Query::equal('deploymentResourceId', [$siteId])->toString(), - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString(), - Query::equal('type', ['deployment'])->toString(), - Query::equal('deploymentId', [$deploymentId])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - }); - } - - public function testCreateSiteBranchRule(): void - { - $domain = \uniqid() . '-site-branch.custom.localhost'; - - $setup = $this->setupSite(); - $siteId = $setup['siteId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($siteId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupSiteRule($domain, $siteId, 'dev'); - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - } - - public function testCreateFunctionBranchRule(): void - { - $domain = \uniqid() . '-function-branch.custom.localhost'; - - $setup = $this->setupFunction(); - $functionId = $setup['functionId']; - $deploymentId = $setup['deploymentId']; - - $this->assertNotEmpty($functionId); - $this->assertNotEmpty($deploymentId); - - $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev'); - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - - $this->cleanupFunction($functionId); - } - - public function testUpdateRule(): void - { - // Create function appwrite-network domain - $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0]; - $domain = \uniqid() . '-cname-api.' . $functionsDomain; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verified', $rule['body']['status']); - - $this->cleanupRule($rule['body']['$id']); - - // Create site appwrite-network domain - $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; - $domain = \uniqid() . '-cname-api.' . $sitesDomain; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verified', $rule['body']['status']); - - $this->cleanupRule($rule['body']['$id']); - - // Create + update - $domain = \uniqid() . '-cname-api.custom.com'; - - $rule = $this->createAPIRule($domain); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $ruleId = $rule['body']['$id']; - - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - - $this->cleanupRule($ruleId); - } - - public function testGetRule() - { - $domain = \uniqid() . '-get.custom.localhost'; - $ruleId = $this->setupAPIRule($domain); - - $this->assertNotEmpty($ruleId); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals($domain, $rule['body']['domain']); - $this->assertEquals('manual', $rule['body']['trigger']); - $this->assertArrayHasKey('$id', $rule['body']); - $this->assertArrayHasKey('domain', $rule['body']); - $this->assertArrayHasKey('type', $rule['body']); - $this->assertArrayHasKey('redirectUrl', $rule['body']); - $this->assertArrayHasKey('redirectStatusCode', $rule['body']); - $this->assertArrayHasKey('deploymentResourceType', $rule['body']); - $this->assertArrayHasKey('deploymentId', $rule['body']); - $this->assertArrayHasKey('deploymentResourceId', $rule['body']); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']); - $this->assertArrayHasKey('logs', $rule['body']); - $this->assertArrayHasKey('renewAt', $rule['body']); - - $this->cleanupRule($ruleId); - } - - public function testListRules() - { - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $rule = $this->deleteRule($rule['$id']); - $this->assertEquals(204, $rule['headers']['status-code']); - } - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - - $rule1Domain = \uniqid() . '-list1.custom.localhost'; - $rule1Id = $this->setupAPIRule($rule1Domain); - $this->assertNotEmpty($rule1Id); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); - - $this->assertEquals('manual', $rules['body']['rules'][0]['trigger']); - $this->assertArrayHasKey('$id', $rules['body']['rules'][0]); - $this->assertArrayHasKey('domain', $rules['body']['rules'][0]); - $this->assertArrayHasKey('type', $rules['body']['rules'][0]); - $this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]); - $this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]); - $this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]); - $this->assertArrayHasKey('logs', $rules['body']['rules'][0]); - $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]); - - $rule2Domain = \uniqid() . '-list1.custom.localhost'; - $rule2Id = $this->setupAPIRule($rule2Domain); - $this->assertNotEmpty($rule2Id); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - $this->assertCount(2, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::limit(1)->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(2, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('$id', [$rule1Id])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); - - $rules = $this->listRules([ - 'queries' => [ - Query::orderDesc('$id')->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(2, $rules['body']['rules']); - $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); - - $rules = $this->listRules([ - 'queries' => [ - Query::equal('domain', [$rule2Domain])->toString() - ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); - - $rules = $this->listRules([ - 'search' => $rule1Domain, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleIds = \array_column($rules['body']['rules'], '$id'); - $this->assertContains($rule1Id, $ruleIds); - - $rules = $this->listRules([ - 'search' => $rule2Domain, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleIds = \array_column($rules['body']['rules'], '$id'); - $this->assertContains($rule2Id, $ruleIds); - - $rules = $this->listRules([ - 'search' => $rule1Id, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleDomains = \array_column($rules['body']['rules'], 'domain'); - $this->assertContains($rule1Domain, $ruleDomains); - - $rules = $this->listRules([ - 'search' => $rule2Id, - 'queries' => [ Query::orderDesc('$createdAt')->toString() ] - ]); - $this->assertEquals(200, $rules['headers']['status-code']); - $ruleDomains = \array_column($rules['body']['rules'], 'domain'); - $this->assertContains($rule2Domain, $ruleDomains); - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - foreach ($rules['body']['rules'] as $rule) { - $rule = $this->deleteRule($rule['$id']); - $this->assertEquals(204, $rule['headers']['status-code']); - } - - $rules = $this->listRules(); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(0, $rules['body']['total']); - $this->assertCount(0, $rules['body']['rules']); - } - - public function testRuleVerification(): void - { - - // 1. Site rule can verify - $site = $this->setupSite(); - $siteId = $site['siteId']; - - $rule = $this->createSiteRule('stage-site.webapp.com', $siteId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - $this->assertNotEmpty($rule['body']['$id']); - $ruleId = $rule['body']['$id']; - - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals($ruleId, $rule['body']['$id']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - $this->cleanupSite($siteId); - - // 2. Function rule can verify - $function = $this->setupFunction(); - $functionId = $function['functionId']; - - $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - $this->cleanupRule($rule['body']['$id']); - - $rule = $this->createAPIRule('stage-site.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); - $this->cleanupRule($rule['body']['$id']); - - $this->cleanupFunction($functionId); - - // 3. Wrong A record fails to verify - $rule = $this->createAPIRule('wrong-a-webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 4. Correct A record can verify - $rule = $this->createAPIRule('webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - - // 5. Correct CNAME record can verify (no CAA record) - $rule = $this->createAPIRule('stage.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - - // 6. Missing CNAME record fails to verify - $rule = $this->createAPIRule('stage-missing-cname.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 7. Wrong CNAME record fails to verify - $rule = $this->createAPIRule('stage-wrong-cname.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 8. Wrong CAA record fails to verify - $rule = $this->createAPIRule('stage-wrong-caa.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); - $this->assertEquals(400, $rule['headers']['status-code']); - $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); - - $rule = $this->getRule($ruleId); - $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - - $this->cleanupRule($ruleId); - - // 9. Correct CAA record can verify - $rule = $this->createAPIRule('stage-correct-caa.webapp.com'); - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('verifying', $rule['body']['status']); - $this->assertEmpty($rule['body']['logs']); - - $this->cleanupRule($rule['body']['$id']); - } - - public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void - { - $domain = \uniqid() . '-timestamp-test.webapp.com'; - $rule = $this->createAPIRule($domain); - - $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); - $this->assertNotEmpty($rule['body']['logs']); - - $ruleId = $rule['body']['$id']; - $initialUpdatedAt = $rule['body']['$updatedAt']; - $initiallogs = $rule['body']['logs']; - - sleep(1); - - $updatedRule = $this->updateRuleVerification($ruleId); - - $this->assertEquals(400, $updatedRule['headers']['status-code']); - $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']); - - $ruleAfterUpdate = $this->getRule($ruleId); - $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']); - $this->assertEquals('created', $ruleAfterUpdate['body']['status']); - $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']); - $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']); - - $initialTime = new \DateTime($initialUpdatedAt); - $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']); - $this->assertGreaterThan($initialTime, $updatedTime); - - $this->cleanupRule($ruleId); - } } diff --git a/tests/e2e/Services/Proxy/ProxyHelpers.php b/tests/e2e/Services/Proxy/ProxyHelpers.php new file mode 100644 index 0000000000..ef962ce725 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyHelpers.php @@ -0,0 +1,293 @@ +client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $rule; + } + + protected function createAPIRule(string $domain): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + ]); + + return $rule; + } + + protected function 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 + { + $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)); + } +} From ec75f101bb0d1082c2aed451ddf763a86a4f4f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 11:37:00 +0200 Subject: [PATCH 27/73] SDK quality fix --- .../Modules/Project/Http/Project/Keys/Ephemeral/Create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php index 7fdefca218..4130effe69 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php @@ -59,7 +59,7 @@ class Create extends Base ], )) ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) - ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false) + ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600) ->inject('response') ->inject('queueForEvents') ->inject('project') From d8349544d20b698e559de53aae0b3bcf38c4e3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 11:37:26 +0200 Subject: [PATCH 28/73] Revert unwanted change --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index d57e7cf421..4a6a3ac344 100644 --- a/.env +++ b/.env @@ -20,7 +20,7 @@ _APP_EMAIL_CERTIFICATES=certificates@appwrite.io _APP_SYSTEM_RESPONSE_FORMAT= _APP_CUSTOM_DOMAIN_DENY_LIST= _APP_OPTIONS_ABUSE=disabled -_APP_OPTIONS_ROUTER_PROTECTION=enabled +_APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key From cd6f5c64f08eff7d5c78cc1efbda528bbcecda17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 11:48:02 +0200 Subject: [PATCH 29/73] Improve proxy API quality --- app/config/scopes/project.php | 18 ++++++++------- .../Modules/Proxy/Http/Rules/API/Create.php | 22 ++++++++++++++---- .../Modules/Proxy/Http/Rules/Delete.php | 11 +++++---- .../Platform/Modules/Proxy/Http/Rules/Get.php | 11 +++++---- .../Proxy/Http/Rules/Verification/Update.php | 23 +++++++++++-------- .../Modules/Proxy/Http/Rules/XList.php | 19 ++++++++------- 6 files changed, 66 insertions(+), 38 deletions(-) diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index a048920de9..5f9e4877b3 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -286,6 +286,16 @@ return [ 'category' => 'Messaging', ], + // Proxy + 'rules.read' => [ + 'description' => 'Access to read proxy rules.', + 'category' => 'Proxy', + ], + 'rules.write' => [ + 'description' => 'Access to create, update, and delete proxy rules.', + 'category' => 'Proxy', + ], + // Other "webhooks.read" => [ "description" => @@ -339,12 +349,4 @@ return [ 'description' => 'Access to create, update, and delete resources under VCS service.', 'category' => 'Other', ], - 'rules.read' => [ - 'description' => 'Access to read proxy rules.', - 'category' => 'Other', - ], - 'rules.write' => [ - 'description' => 'Access to create, update, and delete proxy rules.', - 'category' => 'Other', - ], ]; diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index a6a3e44194..13c06c3e91 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -9,11 +9,13 @@ use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; 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; @@ -47,8 +49,10 @@ class Create extends Action name: 'createAPIRule', description: <<inject('dbForPlatform') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log) - { + public function action( + string $domain, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + array $platform, + Log $log, + Authorization $authorization, + ) { $this->validateDomainRestrictions($domain, $platform); // TODO: (@Meldiron) Remove after 1.7.x migration @@ -108,7 +122,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); } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php index 1d5b770496..2bbe4a530a 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -43,7 +44,7 @@ class Delete extends Action description: <<inject('dbForPlatform') ->inject('queueForDeletes') ->inject('queueForEvents') + ->inject('authorization') ->callback($this->action(...)); } @@ -67,15 +69,16 @@ class Delete extends Action Document $project, Database $dbForPlatform, DeleteEvent $queueForDeletes, - Event $queueForEvents + Event $queueForEvents, + Authorization $authorization, ) { - $rule = $dbForPlatform->getDocument('rules', $ruleId); + $rule = $authorization->skip(fn() => $dbForPlatform->getDocument('rules', $ruleId)); if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) { throw new Exception(Exception::RULE_NOT_FOUND); } - $dbForPlatform->deleteDocument('rules', $rule->getId()); + $authorization->skip(fn() => $dbForPlatform->deleteDocument('rules', $rule->getId())); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php index b88a4ffc06..03841bf1e5 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; @@ -39,7 +40,7 @@ class Get extends Action description: <<inject('response') ->inject('project') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } @@ -58,15 +60,16 @@ class Get extends Action string $ruleId, Response $response, Document $project, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization, ) { - $rule = $dbForPlatform->getDocument('rules', $ruleId); + $rule = $authorization->skip(fn() => $dbForPlatform->getDocument('rules', $ruleId)); if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) { throw new Exception(Exception::RULE_NOT_FOUND); } - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); + $certificate = $authorization->skip(fn() => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''))); // Give priority to certificate generation logs if present if (!empty($certificate->getAttribute('logs', ''))) { diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php index 9e81f6ff18..f567077c23 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php @@ -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; @@ -44,9 +45,9 @@ class Update extends Action group: null, name: 'updateRuleVerification', description: <<inject('project') ->inject('dbForPlatform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } @@ -71,9 +73,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 +93,22 @@ class Update extends Action try { $this->verifyRule($rule, $log); // Reset logs and status for the rule - $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $rule = $authorization->skip(fn() => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ 'logs' => '', 'status' => RULE_STATUS_CERTIFICATE_GENERATING, - ])); + ]))); $certificateId = $rule->getAttribute('certificateId', ''); // Reset logs for the associated certificate. if (!empty($certificateId)) { - $certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ + $certificate = $authorization->skip(fn() => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ 'logs' => '', - ])); + ]))); } } catch (Exception $err) { - $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $authorization->skip(fn() => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ '$updatedAt' => DateTime::now(), - ])); + ]))); throw $err; } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index 19daf8c8d2..655563aa3b 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -13,6 +13,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Query\Cursor; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; @@ -44,7 +45,7 @@ class XList extends Action description: <<param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true, deprecated: true) ->inject('response') ->inject('project') ->inject('dbForPlatform') + ->inject('authorization') ->callback($this->action(...)); } public function action( array $queries, + bool $total, string $search, - bool $includeTotal, Response $response, Document $project, - Database $dbForPlatform + Database $dbForPlatform, + Authorization $authorization, ) { try { $queries = Query::parseQueries($queries); @@ -91,7 +94,7 @@ class XList extends Action } $ruleId = $cursor->getValue(); - $cursorDocument = $dbForPlatform->getDocument('rules', $ruleId); + $cursorDocument = $authorization->skip(fn() => $dbForPlatform->getDocument('rules', $ruleId)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found."); @@ -102,9 +105,9 @@ class XList extends Action $filterQueries = Query::groupByType($queries)['filters']; - $rules = $dbForPlatform->find('rules', $queries); + $rules = $authorization->skip(fn() => $dbForPlatform->find('rules', $queries)); foreach ($rules as $rule) { - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); + $certificate = $authorization->skip(fn() => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''))); // Give priority to certificate generation logs if present if (!empty($certificate->getAttribute('logs', ''))) { @@ -116,7 +119,7 @@ class XList extends Action $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); } } From 22318308260309c8228883a1cdab067fa09cdb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 11:59:04 +0200 Subject: [PATCH 30/73] Further proxy API improvements --- .../Modules/Proxy/Http/Rules/API/Create.php | 12 +++++++++--- .../Modules/Proxy/Http/Rules/Delete.php | 8 ++++---- .../Platform/Modules/Proxy/Http/Rules/Get.php | 13 ++++++++++--- .../Rules/{Verification => Status}/Update.php | 19 ++++++++++--------- .../Modules/Proxy/Http/Rules/XList.php | 19 +++++++++++++------ .../Platform/Modules/Proxy/Services/Http.php | 4 ++-- src/Appwrite/Utopia/Response/Model/Rule.php | 8 ++++---- tests/e2e/Services/Proxy/ProxyBase.php | 14 +++++++------- tests/e2e/Services/Proxy/ProxyHelpers.php | 4 ++-- 9 files changed, 61 insertions(+), 40 deletions(-) rename src/Appwrite/Platform/Modules/Proxy/Http/Rules/{Verification => Status}/Update.php (84%) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index 13c06c3e91..aaf94ef427 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -9,7 +9,6 @@ use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; -use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -45,7 +44,7 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createAPIRule', description: <<skip(fn() => $dbForPlatform->createDocument('rules', $rule)); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -140,6 +139,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php index 2bbe4a530a..29751ff20a 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php @@ -39,7 +39,7 @@ class Delete extends Action ->label('audits.resource', 'rule/{request.ruleId}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'deleteRule', description: <<inject('dbForPlatform') ->inject('queueForDeletes') ->inject('queueForEvents') - ->inject('authorization') + ->inject('authorization') ->callback($this->action(...)); } @@ -72,13 +72,13 @@ class Delete extends Action Event $queueForEvents, Authorization $authorization, ) { - $rule = $authorization->skip(fn() => $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); } - $authorization->skip(fn() => $dbForPlatform->deleteDocument('rules', $rule->getId())); + $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId())); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php index 03841bf1e5..103ab1fddc 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php @@ -35,7 +35,7 @@ class Get extends Action ->label('scope', 'rules.read') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'getRule', description: <<skip(fn() => $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 = $authorization->skip(fn() => $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', ''))) { @@ -78,6 +78,13 @@ class Get extends Action $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response->dynamic($rule, Response::MODEL_PROXY_RULE); } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php similarity index 84% rename from src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php rename to src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php index f567077c23..1ad6f730b3 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/proxy/rules/:ruleId/verification') - ->desc('Update rule verification status') + ->setHttpPath('/v1/proxy/rules/:ruleId/status') + ->httpAlias('/v1/proxy/rules/:ruleId/verification') + ->desc('Update rule status') ->groups(['api', 'proxy']) ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].update') @@ -42,8 +43,8 @@ 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: <<skip(fn() => $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); @@ -93,7 +94,7 @@ class Update extends Action try { $this->verifyRule($rule, $log); // Reset logs and status for the rule - $rule = $authorization->skip(fn() => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $rule = $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ 'logs' => '', 'status' => RULE_STATUS_CERTIFICATE_GENERATING, ]))); @@ -101,12 +102,12 @@ class Update extends Action $certificateId = $rule->getAttribute('certificateId', ''); // Reset logs for the associated certificate. if (!empty($certificateId)) { - $certificate = $authorization->skip(fn() => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ + $certificate = $authorization->skip(fn () => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ 'logs' => '', ]))); } } catch (Exception $err) { - $authorization->skip(fn() => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ '$updatedAt' => DateTime::now(), ]))); throw $err; diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index 655563aa3b..999b4c8d74 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -40,7 +40,7 @@ class XList extends Action ->label('scope', 'rules.read') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'listRules', description: <<inject('response') ->inject('project') ->inject('dbForPlatform') - ->inject('authorization') + ->inject('authorization') ->callback($this->action(...)); } @@ -94,7 +94,7 @@ class XList extends Action } $ruleId = $cursor->getValue(); - $cursorDocument = $authorization->skip(fn() => $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."); @@ -105,9 +105,9 @@ class XList extends Action $filterQueries = Query::groupByType($queries)['filters']; - $rules = $authorization->skip(fn() => $dbForPlatform->find('rules', $queries)); + $rules = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries)); foreach ($rules as $rule) { - $certificate = $authorization->skip(fn() => $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', ''))) { @@ -115,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' => $total ? $authorization->skip(fn() => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0, + 'total' => $total ? $authorization->skip(fn () => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0, ]), Response::MODEL_PROXY_RULE_LIST); } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php index 980c64cc54..b2a9de1933 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php @@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunction use Appwrite\Platform\Modules\Proxy\Http\Rules\Get as GetRule; use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule; use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule; -use Appwrite\Platform\Modules\Proxy\Http\Rules\Verification\Update as UpdateRuleVerification; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Status\Update as UpdateRuleStatus; use Appwrite\Platform\Modules\Proxy\Http\Rules\XList as ListRules; use Utopia\Platform\Service; @@ -26,6 +26,6 @@ class Http extends Service $this->addAction(GetRule::getName(), new GetRule()); $this->addAction(ListRules::getName(), new ListRules()); $this->addAction(DeleteRule::getName(), new DeleteRule()); - $this->addAction(UpdateRuleVerification::getName(), new UpdateRuleVerification()); + $this->addAction(UpdateRuleStatus::getName(), new UpdateRuleStatus()); } } diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php index 1ff854e7ce..d5ea9ee0b7 100644 --- a/src/Appwrite/Utopia/Response/Model/Rule.php +++ b/src/Appwrite/Utopia/Response/Model/Rule.php @@ -74,7 +74,7 @@ class Rule extends Model ]) ->addRule('deploymentResourceId', [ 'type' => self::TYPE_STRING, - 'description' => 'ID deployment\'s resource. Used if type is "deployment"', + 'description' => 'ID of deployment\'s resource (site or function ID). Used if type is "deployment"', 'default' => '', 'example' => 'n3u9feiwmf', ]) @@ -86,10 +86,10 @@ class Rule extends Model ]) ->addRule('status', [ 'type' => self::TYPE_ENUM, - 'description' => 'Domain verification status. Possible values are "created", "verifying", "verified" and "unverified"', - 'default' => 'created', + 'description' => 'Domain verification status. Possible values are "unverified", "verifying", "verified"', + 'default' => 'unverified', 'example' => 'verified', - 'enum' => ['created', 'verifying', 'verified', 'unverified'], + 'enum' => ['unverified', 'verifying', 'verified'], ]) ->addRule('logs', [ 'type' => self::TYPE_STRING, diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 33ef64591b..c33dfbe778 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -425,7 +425,7 @@ trait ProxyBase $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $rule['headers']['status-code']); $this->cleanupRule($ruleId); @@ -599,7 +599,7 @@ trait ProxyBase $this->assertNotEmpty($rule['body']['$id']); $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(200, $rule['headers']['status-code']); $this->assertEquals($ruleId, $rule['body']['$id']); $this->assertEquals('verifying', $rule['body']['status']); @@ -633,7 +633,7 @@ trait ProxyBase $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $rule['headers']['status-code']); $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); @@ -666,7 +666,7 @@ trait ProxyBase $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $rule['headers']['status-code']); $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); @@ -683,7 +683,7 @@ trait ProxyBase $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $rule['headers']['status-code']); $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); @@ -700,7 +700,7 @@ trait ProxyBase $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); $ruleId = $rule['body']['$id']; - $rule = $this->updateRuleVerification($ruleId); + $rule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $rule['headers']['status-code']); $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); @@ -734,7 +734,7 @@ trait ProxyBase sleep(1); - $updatedRule = $this->updateRuleVerification($ruleId); + $updatedRule = $this->updateRuleStatus($ruleId); $this->assertEquals(400, $updatedRule['headers']['status-code']); $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']); diff --git a/tests/e2e/Services/Proxy/ProxyHelpers.php b/tests/e2e/Services/Proxy/ProxyHelpers.php index ef962ce725..6f15abdad8 100644 --- a/tests/e2e/Services/Proxy/ProxyHelpers.php +++ b/tests/e2e/Services/Proxy/ProxyHelpers.php @@ -34,9 +34,9 @@ trait ProxyHelpers return $rule; } - protected function updateRuleVerification(string $ruleId): mixed + protected function updateRuleStatus(string $ruleId): mixed { - $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([ + $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()), []); From 2263632f7d269f2e49e8c6006a048340b7c5becc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 12:06:37 +0200 Subject: [PATCH 31/73] Fix all proxy create endpoints --- .../Proxy/Http/Rules/Function/Create.php | 34 +++++++++++++++--- .../Proxy/Http/Rules/Redirect/Create.php | 36 ++++++++++++++++--- .../Modules/Proxy/Http/Rules/Site/Create.php | 34 +++++++++++++++--- 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index 4a8bd4897e..200182cf03 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -45,12 +46,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createFunctionRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $functionId, + string $branch, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $function = $dbForProject->getDocument('functions', $functionId); @@ -126,7 +143,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -144,6 +161,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index 5964a20772..f5105704ce 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -46,12 +47,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createRedirectRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $url, + int $statusCode, + string $resourceId, + string $resourceType, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $collection = match ($resourceType) { @@ -131,7 +150,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -149,6 +168,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index a9dfa93a49..7b959055dc 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; use Utopia\Platform\Scope\HTTP; @@ -45,12 +46,14 @@ class Create extends Action ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - group: null, + group: 'rules', name: 'createSiteRule', description: <<inject('dbForProject') ->inject('platform') ->inject('log') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) - { + public function action( + string $domain, + string $siteId, + ?string $branch, + Response $response, + Document $project, + Certificate $publisherForCertificates, + Event $queueForEvents, + Database $dbForPlatform, + Database $dbForProject, + array $platform, + Log $log, + Authorization $authorization, + ) { + $this->validateDomainRestrictions($domain, $platform); $site = $dbForProject->getDocument('sites', $siteId); @@ -126,7 +143,7 @@ class Create extends Action } try { - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); } catch (Duplicate $e) { throw new Exception(Exception::RULE_ALREADY_EXISTS); } @@ -144,6 +161,13 @@ class Create extends Action $queueForEvents->setParam('ruleId', $rule->getId()); + // Rename 'created' status to 'unverified' for consistency. + // 'verifying' and 'verified' statuses stay as is. + // 'unverified' in the meaning of failed certificate generation stays as is. + if ($rule->getAttribute('status') === 'created') { + $rule->setAttribute('status', 'unverified'); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($rule, Response::MODEL_PROXY_RULE); From 879dc6873ec3f27e43f8453a1159e362e233082f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 12:07:52 +0200 Subject: [PATCH 32/73] review fixes --- .../Modules/Proxy/Http/Rules/API/Create.php | 2 +- .../Proxy/Http/Rules/Function/Create.php | 2 +- .../Proxy/Http/Rules/Redirect/Create.php | 2 +- .../Modules/Proxy/Http/Rules/Site/Create.php | 2 +- tests/e2e/Services/Proxy/ProxyBase.php | 24 +++++++++---------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index aaf94ef427..6f2e40d13f 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -49,7 +49,7 @@ class Create extends Action description: <<createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); } public function testCreateRuleVcs(): void @@ -421,7 +421,7 @@ trait ProxyBase $rule = $this->createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $ruleId = $rule['body']['$id']; @@ -620,7 +620,7 @@ trait ProxyBase $rule = $this->createAPIRule('stage-site.webapp.com'); $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); $this->cleanupRule($rule['body']['$id']); @@ -629,7 +629,7 @@ trait ProxyBase // 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->assertEquals('unverified', $rule['body']['status']); $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); $ruleId = $rule['body']['$id']; @@ -639,7 +639,7 @@ trait ProxyBase $rule = $this->getRule($ruleId); $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->cleanupRule($ruleId); @@ -662,7 +662,7 @@ trait ProxyBase // 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->assertEquals('unverified', $rule['body']['status']); $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']); $ruleId = $rule['body']['$id']; @@ -672,14 +672,14 @@ trait ProxyBase $rule = $this->getRule($ruleId); $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $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('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']); $ruleId = $rule['body']['$id']; @@ -689,14 +689,14 @@ trait ProxyBase $rule = $this->getRule($ruleId); $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $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('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']); $ruleId = $rule['body']['$id']; @@ -706,7 +706,7 @@ trait ProxyBase $rule = $this->getRule($ruleId); $this->assertEquals(200, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->cleanupRule($ruleId); @@ -725,7 +725,7 @@ trait ProxyBase $rule = $this->createAPIRule($domain); $this->assertEquals(201, $rule['headers']['status-code']); - $this->assertEquals('created', $rule['body']['status']); + $this->assertEquals('unverified', $rule['body']['status']); $this->assertNotEmpty($rule['body']['logs']); $ruleId = $rule['body']['$id']; From d7d0ecb106f385ddeb53d59f4831badfbfa132e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 12:17:34 +0200 Subject: [PATCH 33/73] Fix failing tests --- tests/e2e/Services/Proxy/ProxyBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 307c4b20db..32ee13b5f7 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -290,7 +290,7 @@ trait ProxyBase $this->assertNotEmpty($ruleId); $rule = $this->getRule($ruleId); $this->assertSame(200, $rule['headers']['status-code']); - $this->assertSame('created', $rule['body']['status']); + $this->assertSame('unverified', $rule['body']['status']); $response = $proxyClient->call(Client::METHOD_GET, '/contact'); $this->assertEquals(200, $response['headers']['status-code']); @@ -741,7 +741,7 @@ trait ProxyBase $ruleAfterUpdate = $this->getRule($ruleId); $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']); - $this->assertEquals('created', $ruleAfterUpdate['body']['status']); + $this->assertEquals('unverified', $ruleAfterUpdate['body']['status']); $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']); $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']); From d18f64d526187de8890b876553a21604954fe343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 15:22:23 +0200 Subject: [PATCH 34/73] Fix endpoint consistency, apply request filter alias --- composer.json | 10 +- composer.lock | 193 +++++++++++------- .../Project/Http/Project/OAuth2/Get.php | 12 +- 3 files changed, 126 insertions(+), 89 deletions(-) diff --git a/composer.json b/composer.json index 735955d980..d6182ba750 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "utopia-php/audit": "2.2.*", "utopia-php/auth": "0.5.*", "utopia-php/cache": "1.0.*", - "utopia-php/cli": "0.23.*", + "utopia-php/cli": "dev-feat-param-alias as 0.23.99", "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", @@ -67,7 +67,7 @@ "utopia-php/emails": "0.6.*", "utopia-php/dns": "1.6.*", "utopia-php/dsn": "0.2.1", - "utopia-php/http": "0.34.*", + "utopia-php/http": "dev-feat-param-aliases as 0.34.99", "utopia-php/fetch": "0.5.*", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", @@ -75,12 +75,12 @@ "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.9.*", - "utopia-php/platform": "0.13.*", + "utopia-php/platform": "dev-feat-param-aliases as 0.13.99", "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": "dev-feat-param-aliases as 0.17.99", + "utopia-php/servers": "dev-feat-param-aliases as 0.3.99", "utopia-php/registry": "0.5.*", "utopia-php/storage": "2.*", "utopia-php/system": "0.10.*", diff --git a/composer.lock b/composer.lock index 9f34d2dbe1..89ed556028 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bd45829c252971301370d62300be106d", + "content-hash": "ff46a826bc257b5ed2f0a8c821d8a0c4", "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": "dev-feat-param-alias", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab" + "reference": "c01a4af02249f20f5a9b3e23b6e8ee20f52857a2" }, "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/c01a4af02249f20f5a9b3e23b6e8ee20f52857a2", + "reference": "c01a4af02249f20f5a9b3e23b6e8ee20f52857a2", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/servers": "0.3.*" + "utopia-php/servers": "dev-feat-param-aliases" }, "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/feat-param-alias" }, - "time": "2026-04-27T09:19:04+00:00" + "time": "2026-05-04T12:39:30+00:00" }, { "name": "utopia-php/compression", @@ -3850,22 +3850,23 @@ }, { "name": "utopia-php/database", - "version": "5.4.1", + "version": "5.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93" + "reference": "609ebcd64be1ec6fab00c5f46fce54acb0031b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/688d9422b5ff42ac2ecc29397d94891cfd772e93", - "reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93", + "url": "https://api.github.com/repos/utopia-php/database/zipball/609ebcd64be1ec6fab00c5f46fce54acb0031b3c", + "reference": "609ebcd64be1ec6fab00c5f46fce54acb0031b3c", "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.1" + "source": "https://github.com/utopia-php/database/tree/5.6.0" }, - "time": "2026-04-29T07:32:59+00:00" + "time": "2026-05-01T01:28:07+00:00" }, { "name": "utopia-php/detector", @@ -4271,23 +4272,23 @@ }, { "name": "utopia-php/http", - "version": "0.34.24", + "version": "dev-feat-param-aliases", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33" + "reference": "4439705c8c2e81af826f4b6d694469c9ebc84a9f" }, "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/4439705c8c2e81af826f4b6d694469c9ebc84a9f", + "reference": "4439705c8c2e81af826f4b6d694469c9ebc84a9f", "shasum": "" }, "require": { "php": ">=8.3", "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", - "utopia-php/servers": "0.3.*", + "utopia-php/servers": "dev-feat-param-aliases as 0.3.x-dev", "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/feat-param-aliases" }, - "time": "2026-04-24T12:16:53+00:00" + "time": "2026-05-04T11:52:49+00:00" }, { "name": "utopia-php/image", @@ -4530,16 +4531,16 @@ }, { "name": "utopia-php/migration", - "version": "1.9.5", + "version": "1.9.6", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "952a4dfe232702f80e45c35129466a8d8cb4c599" + "reference": "b164631404ec759f8c368fe2321f44c22bc258ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599", - "reference": "952a4dfe232702f80e45c35129466a8d8cb4c599", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/b164631404ec759f8c368fe2321f44c22bc258ab", + "reference": "b164631404ec759f8c368fe2321f44c22bc258ab", "shasum": "" }, "require": { @@ -4579,9 +4580,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.9.5" + "source": "https://github.com/utopia-php/migration/tree/1.9.6" }, - "time": "2026-04-29T11:19:13+00:00" + "time": "2026-04-30T08:11:07+00:00" }, { "name": "utopia-php/mongo", @@ -4646,26 +4647,26 @@ }, { "name": "utopia-php/platform", - "version": "0.13.0", + "version": "dev-feat-param-aliases", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "d23af5349a7ea9ee11f9920a13626226f985522e" + "reference": "7fa008758c869671014ae4c4d52bb8cd686212f9" }, "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/7fa008758c869671014ae4c4d52bb8cd686212f9", + "reference": "7fa008758c869671014ae4c4d52bb8cd686212f9", "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": "dev-feat-param-alias as 0.23.99", + "utopia-php/http": "dev-feat-param-aliases as 0.34.99", + "utopia-php/queue": "dev-feat-param-aliases as 0.18.99", + "utopia-php/servers": "dev-feat-param-aliases as 0.3.99" }, "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/feat-param-aliases" }, - "time": "2026-04-17T09:57:18+00:00" + "time": "2026-05-04T13:12:58+00:00" }, { "name": "utopia-php/pools", @@ -4803,25 +4804,24 @@ }, { "name": "utopia-php/queue", - "version": "0.17.0", + "version": "dev-feat-param-aliases", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f" + "reference": "32b0c439a8fa4fc06b8b8793800a04be894bf03c" }, "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/32b0c439a8fa4fc06b8b8793800a04be894bf03c", + "reference": "32b0c439a8fa4fc06b8b8793800a04be894bf03c", "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": "dev-feat-param-aliases as 0.3.99", "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/feat-param-aliases" }, - "time": "2026-03-23T16:21:31+00:00" + "time": "2026-05-04T12:59:54+00:00" }, { "name": "utopia-php/registry", @@ -4922,16 +4922,16 @@ }, { "name": "utopia-php/servers", - "version": "0.3.0", + "version": "dev-feat-param-aliases", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "235be31200df9437fc96a1c270ffef4c64fafe52" + "reference": "24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa" }, "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/24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa", + "reference": "24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa", "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/feat-param-aliases" }, - "time": "2026-03-13T11:31:42+00:00" + "time": "2026-05-04T11:57:44+00:00" }, { "name": "utopia-php/span", @@ -5465,16 +5465,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.25.1", + "version": "1.26.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "f21a556b9acdbf75bbdcdc90a078af641646eade" + "reference": "c14a7232ef0f22f594453730407c2719a2854973" }, "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/c14a7232ef0f22f594453730407c2719a2854973", + "reference": "c14a7232ef0f22f594453730407c2719a2854973", "shasum": "" }, "require": { @@ -5510,9 +5510,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.26.0" }, - "time": "2026-04-28T11:12:22+00:00" + "time": "2026-05-04T09:46:22+00:00" }, { "name": "brianium/paratest", @@ -6619,16 +6619,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": { @@ -6697,7 +6697,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": [ { @@ -6705,7 +6705,7 @@ "type": "other" } ], - "time": "2026-04-18T06:12:49+00:00" + "time": "2026-05-01T04:21:04+00:00" }, { "name": "sebastian/cli-parser", @@ -7690,16 +7690,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": { @@ -7756,7 +7756,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.8" + "source": "https://github.com/symfony/console/tree/v8.0.9" }, "funding": [ { @@ -7776,7 +7776,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-04-29T15:02:55+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8442,9 +8442,46 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/cli", + "version": "dev-feat-param-alias", + "alias": "0.23.99", + "alias_normalized": "0.23.99.0" + }, + { + "package": "utopia-php/http", + "version": "dev-feat-param-aliases", + "alias": "0.34.99", + "alias_normalized": "0.34.99.0" + }, + { + "package": "utopia-php/platform", + "version": "dev-feat-param-aliases", + "alias": "0.13.99", + "alias_normalized": "0.13.99.0" + }, + { + "package": "utopia-php/queue", + "version": "dev-feat-param-aliases", + "alias": "0.17.99", + "alias_normalized": "0.17.99.0" + }, + { + "package": "utopia-php/servers", + "version": "dev-feat-param-aliases", + "alias": "0.3.99", + "alias_normalized": "0.3.99.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/cli": 20, + "utopia-php/http": 20, + "utopia-php/platform": 20, + "utopia-php/queue": 20, + "utopia-php/servers": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index ae46a59c67..97358548d6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -11,7 +11,7 @@ use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; class Get extends Action { @@ -86,28 +86,28 @@ class Get extends Action ) ] )) - ->param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.') + ->param('providerId', '', new WhiteList(Config::getParam('oAuthProviders', [])), '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()); From b1c3fc090885e0782c6a9f99f159b7e6ebb330d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 15:40:32 +0200 Subject: [PATCH 35/73] Update tests --- tests/e2e/Services/Project/OAuth2Base.php | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5451435c3c..5286e3f653 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -188,6 +188,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', [ @@ -2573,7 +2595,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,7 +2608,7 @@ trait OAuth2Base return $this->client->call( Client::METHOD_GET, - '/project/oauth2/' . $provider, + '/project/oauth2/' . $providerId, $headers, ); } From d2922e7d5d064f88fd4dc0078be63041d65470e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 15:49:05 +0200 Subject: [PATCH 36/73] Fix failing tests --- .../Modules/Project/Http/Project/OAuth2/Get.php | 2 +- tests/e2e/Services/Project/OAuth2Base.php | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 97358548d6..250a3e5df1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -86,7 +86,7 @@ class Get extends Action ) ] )) - ->param('providerId', '', new WhiteList(Config::getParam('oAuthProviders', [])), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider']) + ->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(...)); diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5286e3f653..97ffc4a50b 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -243,19 +243,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']); From c35d5e348b5d01ae2619961e8b38c931e5bb908c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 16:05:46 +0200 Subject: [PATCH 37/73] Support queries in oauth list endpoint --- .../Project/Http/Project/OAuth2/XList.php | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php index d0780e4bae..6a84868286 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php @@ -2,14 +2,21 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2; +use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Database\Document; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Queries; +use Utopia\Database\Validator\Query\Limit; +use Utopia\Database\Validator\Query\Offset; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Action { @@ -43,15 +50,28 @@ class XList extends Action ) ] )) + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('project') ->callback($this->action(...)); } + /** + * @param array $queries + */ public function action( + array $queries, + bool $includeTotal, Response $response, Document $project, ): void { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + $providers = Config::getParam('oAuthProviders', []); $actions = Base::getProviderActions(); @@ -66,8 +86,16 @@ class XList extends Action $documents[] = $action->buildReadResponse($project); } + $total = $includeTotal ? \count($documents) : 0; + + $grouped = Query::groupByType($queries); + $offset = $grouped['offset'] ?? 0; + $limit = $grouped['limit'] ?? null; + + $documents = \array_slice($documents, $offset, $limit); + $response->dynamic(new Document([ - 'total' => \count($documents), + 'total' => $total, 'providers' => $documents, ]), Response::MODEL_OAUTH2_PROVIDER_LIST); } From 4d5bb309179d12cec7fb4bf1b45f721c1db85fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 4 May 2026 16:05:56 +0200 Subject: [PATCH 38/73] tests for oauth list endpoint ueries and total --- tests/e2e/Services/Project/OAuth2Base.php | 48 ++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5451435c3c..955f95c608 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Project; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use Tests\E2E\Client; +use Utopia\Database\Query; trait OAuth2Base { @@ -172,6 +173,40 @@ trait OAuth2Base $this->assertNotContains('mock-unverified', $ids); } + public function testListOAuth2ProvidersTotalFalse(): void + { + $response = $this->listOAuth2Providers(total: false); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(0, $response['body']['total']); + $this->assertGreaterThan(0, \count($response['body']['providers'])); + } + + public function testListOAuth2ProvidersWithLimit(): void + { + $response = $this->listOAuth2Providers([ + Query::limit(1)->toString(), + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['providers']); + $this->assertGreaterThan(1, $response['body']['total']); + } + + public function testListOAuth2ProvidersWithOffset(): void + { + $listAll = $this->listOAuth2Providers(); + $this->assertSame(200, $listAll['headers']['status-code']); + + $listOffset = $this->listOAuth2Providers([ + Query::offset(1)->toString(), + ]); + + $this->assertSame(200, $listOffset['headers']['status-code']); + $this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']); + $this->assertSame($listAll['body']['total'], $listOffset['body']['total']); + } + // ========================================================================= // Get OAuth2 provider // ========================================================================= @@ -2591,8 +2626,18 @@ trait OAuth2Base ); } - 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 +2651,7 @@ trait OAuth2Base Client::METHOD_GET, '/project/oauth2', $headers, + $params, ); } } From b0220292a706c8256c0c61c6a8d506f37cd97ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 12:58:06 +0200 Subject: [PATCH 39/73] Rename policies to prevent double scope usease --- app/config/roles.php | 4 ++-- app/config/scopes/project.php | 14 +++++++++++++- .../Modules/Project/Http/Project/Policies/Get.php | 2 +- .../Project/Http/Project/Policies/XList.php | 2 +- src/Appwrite/Platform/Workers/Migrations.php | 4 ++-- tests/e2e/Scopes/ProjectCustom.php | 4 ++-- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index 8fba27e503..04175ac1d5 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -59,8 +59,8 @@ $admins = [ 'oauth2.write', 'mocks.read', 'mocks.write', - 'policies.read', - 'policies.write', + 'project.policies.read', + 'project.policies.write', 'templates.read', 'templates.write', 'projects.write', diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index a048920de9..6e019e7b93 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -44,11 +44,23 @@ return [ "category" => "Project", ], "policies.read" => [ + "description" => + "Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control.", + "category" => "Project", + 'deprecated' => true, + ], + "policies.write" => [ + "description" => + "Access to update project\'s policies. Replaces by \'project.policies.read\' for more granular control.", + "category" => "Project", + 'deprecated' => true, + ], + "project.policies.read" => [ "description" => "Access to read project\'s policies", "category" => "Project", ], - "policies.write" => [ + "project.policies.write" => [ "description" => "Access to update project\'s policies", "category" => "Project", diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php index 3ffe30f1fa..21342332d9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php @@ -27,7 +27,7 @@ class Get extends Action ->setHttpPath('/v1/project/policies/:policyId') ->desc('Get project policy') ->groups(['api', 'project']) - ->label('scope', 'policies.read') + ->label('scope', ['policies.read', 'project.policies.read']) ->label('sdk', new Method( namespace: 'project', group: 'policies', diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php index 893b28fef2..3020fa79dd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php @@ -33,7 +33,7 @@ class XList extends Action ->setHttpPath('/v1/project/policies') ->desc('List project policies') ->groups(['api', 'project']) - ->label('scope', 'policies.read') + ->label('scope', ['policies.read', 'project.policies.read']) ->label('sdk', new Method( namespace: 'project', group: 'policies', diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index c25a83c231..3fd86baea9 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -380,8 +380,8 @@ class Migrations extends Action 'oauth2.write', 'mocks.read', 'mocks.write', - 'policies.read', - 'policies.write', + 'project.policies.read', + 'project.policies.write', 'templates.read', 'templates.write', ]; diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 3071ddfa2a..99219ebf99 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -173,8 +173,8 @@ trait ProjectCustom 'oauth2.write', 'mocks.read', 'mocks.write', - 'policies.read', - 'policies.write', + 'project.policies.read', + 'project.policies.write', 'templates.read', 'templates.write', ], From 32d30dfd9d1e9a823611296859afa5168b3a0639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 13:06:57 +0200 Subject: [PATCH 40/73] Fix copy --- app/config/scopes/project.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index 6e019e7b93..7a61524b87 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -45,13 +45,13 @@ return [ ], "policies.read" => [ "description" => - "Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control.", + "Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control", "category" => "Project", 'deprecated' => true, ], "policies.write" => [ "description" => - "Access to update project\'s policies. Replaces by \'project.policies.read\' for more granular control.", + "Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control", "category" => "Project", 'deprecated' => true, ], From fef4cbf3b04006f7f96879e6a6c2118bad20953f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 13:37:51 +0200 Subject: [PATCH 41/73] Fix missing new scope --- .../Project/Http/Project/Policies/MembershipPrivacy/Update.php | 2 +- .../Project/Http/Project/Policies/PasswordDictionary/Update.php | 2 +- .../Project/Http/Project/Policies/PasswordHistory/Update.php | 2 +- .../Http/Project/Policies/PasswordPersonalData/Update.php | 2 +- .../Project/Http/Project/Policies/SessionAlert/Update.php | 2 +- .../Project/Http/Project/Policies/SessionDuration/Update.php | 2 +- .../Http/Project/Policies/SessionInvalidation/Update.php | 2 +- .../Project/Http/Project/Policies/SessionLimit/Update.php | 2 +- .../Modules/Project/Http/Project/Policies/UserLimit/Update.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php index c947ff225a..41a6168b07 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/memberships-privacy') ->desc('Update membership privacy policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php index e2c678abb6..d7ee99fbfe 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/password-dictionary') ->desc('Update password dictionary policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php index a8ae81caff..84861a19e1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/password-history') ->desc('Update password history policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php index 9db7cf0549..435f00fc39 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/personal-data') ->desc('Update password personal data policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php index 22b7a44b04..79653d46ad 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/session-alerts') ->desc('Update session alert policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php index ba72c93a6f..0a7f33218a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/duration') ->desc('Update session duration policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php index 8f8a959959..a1feb67346 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/session-invalidation') ->desc('Update session invalidation policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php index 382ed6f0d9..936a541249 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/max-sessions') ->desc('Update session limit policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php index 9129b81250..2b7e704853 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php @@ -32,7 +32,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId/auth/limit') ->desc('Update user limit policy') ->groups(['api', 'project']) - ->label('scope', 'policies.write') + ->label('scope', ['policies.write', 'project.policies.write']) ->label('event', 'projects.[projectId].policies.[policy].update') ->label('audits.event', 'projects.[projectId].policies.[policy].update') ->label('audits.resource', 'project/{response.$id}') From 90e681a9066da41177dd9a2211cee4c126df2069 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 5 May 2026 19:20:47 +0530 Subject: [PATCH 42/73] updated --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index d8c19b75ac..eb6212a620 100644 --- a/composer.lock +++ b/composer.lock @@ -3854,12 +3854,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "dd1077a1548a2a0a07469181e3421f35e27daf3b" + "reference": "ee8895a3ae835978fa4eb143619e4950e7001648" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/dd1077a1548a2a0a07469181e3421f35e27daf3b", - "reference": "dd1077a1548a2a0a07469181e3421f35e27daf3b", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ee8895a3ae835978fa4eb143619e4950e7001648", + "reference": "ee8895a3ae835978fa4eb143619e4950e7001648", "shasum": "" }, "require": { @@ -3906,7 +3906,7 @@ "issues": "https://github.com/utopia-php/database/issues", "source": "https://github.com/utopia-php/database/tree/big-init" }, - "time": "2026-05-05T13:22:35+00:00" + "time": "2026-05-05T13:27:52+00:00" }, { "name": "utopia-php/detector", From 57dd7cf95229157828398adb06e6f428e79d999b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 5 May 2026 19:22:00 +0530 Subject: [PATCH 43/73] updated --- src/Appwrite/Utopia/Response/Model/AttributeBigInt.php | 1 + src/Appwrite/Utopia/Response/Model/ColumnBigInt.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php index 217589c578..baa93ff103 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php @@ -41,6 +41,7 @@ class AttributeBigInt extends Attribute ]) ->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, diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php index e001b6aedf..895356dbf2 100644 --- a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php +++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php @@ -41,6 +41,7 @@ class ColumnBigInt extends Column ]) ->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, From 7ccf2b1a1e5f70915dd4083ebe0105e5c5475127 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 5 May 2026 19:28:28 +0530 Subject: [PATCH 44/73] updated --- .../Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php | 2 +- .../Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php index 1b7b33291b..1d32c6bad9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -34,7 +34,7 @@ class Create extends BigIntCreate ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint') ->desc('Create bigint column') ->groups(['api', 'database', 'schema']) - ->label('scope', ['tables.write', 'collections.write']) + ->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') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php index 387dd08238..b2754a2b7d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php @@ -35,7 +35,7 @@ class Update extends BigIntUpdate ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key') ->desc('Update bigint column') ->groups(['api', 'database', 'schema']) - ->label('scope', ['tables.write', 'collections.write']) + ->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') From fd1a4542e78ff688e5ad358e827221e397b80ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:09:17 +0200 Subject: [PATCH 45/73] Fix variables API feature parity --- .../Functions/Http/Variables/Create.php | 11 +++- .../Functions/Http/Variables/Delete.php | 6 ++ .../Functions/Http/Variables/Update.php | 40 +++++++---- .../Functions/Http/Variables/XList.php | 66 +++++++++++++++++-- .../Modules/Sites/Http/Variables/Create.php | 11 +++- .../Modules/Sites/Http/Variables/Delete.php | 7 +- .../Modules/Sites/Http/Variables/Update.php | 40 +++++++---- .../Modules/Sites/Http/Variables/XList.php | 59 ++++++++++++++++- 8 files changed, 204 insertions(+), 36 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index fee5b0095d..a33d25b630 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -2,11 +2,13 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -38,6 +40,7 @@ class Create extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].create') ->label('audits.event', 'variable.create') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -56,10 +59,12 @@ class Create extends Base ] )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') @@ -69,10 +74,12 @@ class Create extends Base public function action( string $functionId, + string $variableId, string $key, string $value, bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, @@ -84,7 +91,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $variableId = ID::unique(); + $variableId = ($variableId === '' || $variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ @@ -120,6 +127,8 @@ class Create extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php index f6d77c2a0d..fa9f19ba8f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -35,6 +36,7 @@ class Delete extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'variable.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -56,6 +58,7 @@ class Delete extends Base ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('authorization') @@ -66,6 +69,7 @@ class Delete extends Base string $functionId, string $variableId, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Authorization $authorization @@ -98,6 +102,8 @@ class Delete extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php index 54d7a647a3..6413b29f82 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -38,6 +39,7 @@ class Update extends Base ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'variables.[variableId].update') ->label('audits.event', 'variable.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( @@ -57,10 +59,11 @@ class Update extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) - ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) + ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('authorization') @@ -70,10 +73,11 @@ class Update extends Base public function action( string $functionId, string $variableId, - string $key, + ?string $key, ?string $value, ?bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject, Database $dbForPlatform, Authorization $authorization @@ -93,19 +97,27 @@ class Update extends Base throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); } - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) - ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + if (\is_null($key) && \is_null($value) && \is_null($secret)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID); + } + + $updates = new Document(); + + if (!\is_null($key)) { + $updates->setAttribute('key', $key); + $updates->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + } + + if (!\is_null($value)) { + $updates->setAttribute('value', $value); + } + + if (!\is_null($secret)) { + $updates->setAttribute('secret', $secret); + } try { - $dbForProject->updateDocument('variables', $variable->getId(), new Document([ - 'key' => $key, - 'value' => $value ?? $variable->getAttribute('value'), - 'secret' => $secret ?? $variable->getAttribute('secret'), - 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), - ])); + $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } @@ -125,6 +137,8 @@ class Update extends Base 'active' => $schedule->getAttribute('active'), ]))); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->dynamic($variable, Response::MODEL_VARIABLE); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php index 55dea3be1e..b330812b96 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php @@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\Queries\Variables; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Order as OrderException; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Base { @@ -51,22 +57,74 @@ class XList extends Base ) ) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) + ->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); } - public function action(string $functionId, Response $response, Database $dbForProject) - { + /** + * @param array $queries + */ + public function action( + string $functionId, + array $queries, + bool $includeTotal, + Response $response, + Database $dbForProject + ) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $queries[] = Query::equal('resourceType', ['function']); + $queries[] = Query::equal('resourceInternalId', [$function->getSequence()]); + $queries[] = Query::orderAsc(); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $variableId = $cursor->getValue(); + $cursorDocument = $dbForProject->findOne('variables', [ + Query::equal('$id', [$variableId]), + Query::equal('resourceType', ['function']), + Query::equal('resourceInternalId', [$function->getSequence()]), + ]); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + try { + $variables = $dbForProject->find('variables', $queries); + $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + $response->dynamic(new Document([ - 'variables' => $function->getAttribute('vars', []), - 'total' => \count($function->getAttribute('vars', [])), + 'variables' => $variables, + 'total' => $total, ]), Response::MODEL_VARIABLE_LIST); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 04b30fbc9c..92253d4350 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -2,11 +2,13 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -36,6 +38,7 @@ class Create extends Base ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'variables.[variableId].create') ->label('audits.event', 'variable.create') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( @@ -54,16 +57,18 @@ class Create extends Base ] )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->callback($this->action(...)); } - public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project) + public function action(string $siteId, string $variableId, string $key, string $value, bool $secret, Response $response, QueueEvent $queueForEvents, Database $dbForProject, Document $project) { $site = $dbForProject->getDocument('sites', $siteId); @@ -71,7 +76,7 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - $variableId = ID::unique(); + $variableId = ($variableId === '' || $variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ @@ -96,6 +101,8 @@ class Create extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index d61c9892cf..74c638bddc 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -33,6 +34,7 @@ class Delete extends Base ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'variable.delete') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( @@ -54,11 +56,12 @@ class Delete extends Base ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->callback($this->action(...)); } - public function action(string $siteId, string $variableId, Response $response, Database $dbForProject) + public function action(string $siteId, string $variableId, Response $response, QueueEvent $queueForEvents, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); @@ -77,6 +80,8 @@ class Delete extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index 08cdd4ac38..0ed7414b9d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Variables; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -35,6 +36,7 @@ class Update extends Base ->desc('Update variable') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('event', 'variables.[variableId].update') ->label('audits.event', 'variable.update') ->label('audits.resource', 'site/{request.siteId}') ->label('resourceType', RESOURCE_TYPE_SITES) @@ -55,10 +57,11 @@ class Update extends Base )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) - ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) + ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') + ->inject('queueForEvents') ->inject('dbForProject') ->callback($this->action(...)); } @@ -66,10 +69,11 @@ class Update extends Base public function action( string $siteId, string $variableId, - string $key, + ?string $key, ?string $value, ?bool $secret, Response $response, + QueueEvent $queueForEvents, Database $dbForProject ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -87,19 +91,27 @@ class Update extends Base throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); } - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) - ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); + if (\is_null($key) && \is_null($value) && \is_null($secret)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID); + } + + $updates = new Document(); + + if (!\is_null($key)) { + $updates->setAttribute('key', $key); + $updates->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); + } + + if (!\is_null($value)) { + $updates->setAttribute('value', $value); + } + + if (!\is_null($secret)) { + $updates->setAttribute('secret', $secret); + } try { - $dbForProject->updateDocument('variables', $variable->getId(), new Document([ - 'key' => $variable->getAttribute('key'), - 'value' => $variable->getAttribute('value'), - 'secret' => $variable->getAttribute('secret'), - 'search' => $variable->getAttribute('search'), - ])); + $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } @@ -108,6 +120,8 @@ class Update extends Base 'live' => false, ])); + $queueForEvents->setParam('variableId', $variable->getId()); + $response->dynamic($variable, Response::MODEL_VARIABLE); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php index 669aa8be98..1270fe4925 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php @@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Validator\Queries\Variables; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Order as OrderException; +use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Query; +use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; class XList extends Base { @@ -51,13 +57,20 @@ class XList extends Base ) ) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) + ->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); } + /** + * @param array $queries + */ public function action( string $siteId, + array $queries, + bool $includeTotal, Response $response, Database $dbForProject ) { @@ -67,9 +80,51 @@ class XList extends Base throw new Exception(Exception::SITE_NOT_FOUND); } + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $queries[] = Query::equal('resourceType', ['site']); + $queries[] = Query::equal('resourceInternalId', [$site->getSequence()]); + $queries[] = Query::orderAsc(); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $variableId = $cursor->getValue(); + $cursorDocument = $dbForProject->findOne('variables', [ + Query::equal('$id', [$variableId]), + Query::equal('resourceType', ['site']), + Query::equal('resourceInternalId', [$site->getSequence()]), + ]); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + try { + $variables = $dbForProject->find('variables', $queries); + $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + $response->dynamic(new Document([ - 'variables' => $site->getAttribute('vars', []), - 'total' => \count($site->getAttribute('vars', [])), + 'variables' => $variables, + 'total' => $total, ]), Response::MODEL_VARIABLE_LIST); } } From 93515fcc1d69f8e8e89c835dfa8aee9c8d98bfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:17:47 +0200 Subject: [PATCH 46/73] Finalize variables rework --- .../Platform/Modules/Functions/Http/Variables/Create.php | 4 ++-- .../Modules/Project/Http/Project/Variables/Create.php | 3 ++- .../Modules/Project/Http/Project/Variables/Delete.php | 3 ++- .../Platform/Modules/Project/Http/Project/Variables/Get.php | 3 ++- .../Modules/Project/Http/Project/Variables/Update.php | 3 ++- .../Platform/Modules/Project/Http/Project/Variables/XList.php | 1 + src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php | 4 ++-- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index a33d25b630..e619e28928 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -59,7 +59,7 @@ class Create extends Base ] )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject']) - ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) @@ -91,7 +91,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $variableId = ($variableId === '' || $variableId === 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php index 8dbc720045..c9af0b25e0 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php @@ -35,6 +35,7 @@ class Create extends Action ->desc('Create project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') + ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].create') ->label('audits.event', 'project.variable.create') ->label('audits.resource', 'project.variable/{response.$id}') @@ -53,7 +54,7 @@ class Create extends Action ) ], )) - ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.') ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.') ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php index 2b0ae8feb1..5e268b0e36 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php @@ -32,6 +32,7 @@ class Delete extends Action ->desc('Delete project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') + ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'project.variable.delete') ->label('audits.resource', 'project.variable/{request.variableId}') @@ -51,7 +52,7 @@ class Delete extends Action ], contentType: ContentType::NONE )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php index af14148c92..7cbd4456a7 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php @@ -29,6 +29,7 @@ class Get extends Action ->desc('Get project variable') ->groups(['api', 'project']) ->label('scope', 'project.read') + ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('sdk', new Method( namespace: 'project', group: 'variables', @@ -44,7 +45,7 @@ class Get extends Action ) ] )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php index 988a7c0849..66cfb60740 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php @@ -34,6 +34,7 @@ class Update extends Action ->desc('Update project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') + ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].update') ->label('audits.event', 'project.variable.update') ->label('audits.resource', 'project.variable/{response.$id}') @@ -52,7 +53,7 @@ class Update extends Action ) ] )) - ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject']) ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true) ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true) ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php index bd391ea3b4..582fed93c9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php @@ -35,6 +35,7 @@ class XList extends Action ->desc('List project variables') ->groups(['api', 'project']) ->label('scope', 'project.read') + ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('sdk', new Method( namespace: 'project', group: 'variables', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 92253d4350..da58f84f0b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -57,7 +57,7 @@ class Create extends Base ] )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject']) - ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForProject']) + ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) @@ -76,7 +76,7 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - $variableId = ($variableId === '' || $variableId === 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ From 305e8e1ec719a516f3cb710988c33dd482887e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:24:41 +0200 Subject: [PATCH 47/73] Add request filter --- app/controllers/general.php | 8 ++++++ app/init/constants.php | 2 +- src/Appwrite/Migration/Migration.php | 1 + src/Appwrite/Utopia/Request/Filters/V25.php | 27 ++++++++++++++++++++ src/Appwrite/Utopia/Response/Filters/V25.php | 17 ++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/Appwrite/Utopia/Request/Filters/V25.php create mode 100644 src/Appwrite/Utopia/Response/Filters/V25.php diff --git a/app/controllers/general.php b/app/controllers/general.php index eb4899a3d8..11cae2af88 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -28,6 +28,7 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21; use Appwrite\Utopia\Request\Filters\V22 as RequestV22; use Appwrite\Utopia\Request\Filters\V23 as RequestV23; use Appwrite\Utopia\Request\Filters\V24 as RequestV24; +use Appwrite\Utopia\Request\Filters\V25 as RequestV25; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -38,6 +39,7 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21; use Appwrite\Utopia\Response\Filters\V22 as ResponseV22; use Appwrite\Utopia\Response\Filters\V23 as ResponseV23; use Appwrite\Utopia\Response\Filters\V24 as ResponseV24; +use Appwrite\Utopia\Response\Filters\V25 as ResponseV25; use Appwrite\Utopia\View; use Executor\Executor; use MaxMind\Db\Reader; @@ -904,6 +906,9 @@ Http::init() if (version_compare($requestFormat, '1.9.3', '<')) { $request->addFilter(new RequestV24()); } + if (version_compare($requestFormat, '1.9.4', '<')) { + $request->addFilter(new RequestV25()); + } } $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); @@ -931,6 +936,9 @@ Http::init() if (version_compare($responseFormat, '1.9.3', '<')) { $response->addFilter(new ResponseV24()); } + if (version_compare($responseFormat, '1.9.4', '<')) { + $response->addFilter(new ResponseV25()); + } if (version_compare($responseFormat, '1.9.2', '<')) { $response->addFilter(new ResponseV23()); } diff --git a/app/init/constants.php b/app/init/constants.php index f27d0c7c70..64635bab2a 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -45,7 +45,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4324; -const APP_VERSION_STABLE = '1.9.3'; +const APP_VERSION_STABLE = '1.9.4'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 359925e368..004e09cd23 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -96,6 +96,7 @@ abstract class Migration '1.9.1' => 'V24', '1.9.2' => 'V24', '1.9.3' => 'V24', + '1.9.4' => 'V25', ]; /** diff --git a/src/Appwrite/Utopia/Request/Filters/V25.php b/src/Appwrite/Utopia/Request/Filters/V25.php new file mode 100644 index 0000000000..cba70a5f7b --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V25.php @@ -0,0 +1,27 @@ +fillVariableId($content); + break; + } + + return $content; + } + + protected function fillVariableId(array $content): array + { + $content['variableId'] = $content['variableId'] ?? 'unique()'; + return $content; + } +} diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php new file mode 100644 index 0000000000..03d19d2cc1 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -0,0 +1,17 @@ + $content, + }; + } +} From 9b120e9db7a62373988ee3ce31446d50c8f02c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:29:09 +0200 Subject: [PATCH 48/73] Remove unwanted changes --- .../Platform/Modules/Project/Http/Project/Variables/Create.php | 1 - .../Platform/Modules/Project/Http/Project/Variables/Delete.php | 1 - .../Platform/Modules/Project/Http/Project/Variables/Get.php | 1 - .../Platform/Modules/Project/Http/Project/Variables/Update.php | 1 - .../Platform/Modules/Project/Http/Project/Variables/XList.php | 1 - 5 files changed, 5 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php index c9af0b25e0..f4e27def72 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php @@ -35,7 +35,6 @@ class Create extends Action ->desc('Create project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') - ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].create') ->label('audits.event', 'project.variable.create') ->label('audits.resource', 'project.variable/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php index 5e268b0e36..553fb09e54 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php @@ -32,7 +32,6 @@ class Delete extends Action ->desc('Delete project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') - ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].delete') ->label('audits.event', 'project.variable.delete') ->label('audits.resource', 'project.variable/{request.variableId}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php index 7cbd4456a7..d9030421d7 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php @@ -29,7 +29,6 @@ class Get extends Action ->desc('Get project variable') ->groups(['api', 'project']) ->label('scope', 'project.read') - ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('sdk', new Method( namespace: 'project', group: 'variables', diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php index 66cfb60740..6b05e19a78 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php @@ -34,7 +34,6 @@ class Update extends Action ->desc('Update project variable') ->groups(['api', 'project']) ->label('scope', 'project.write') - ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('event', 'variables.[variableId].update') ->label('audits.event', 'project.variable.update') ->label('audits.resource', 'project.variable/{response.$id}') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php index 582fed93c9..bd391ea3b4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php @@ -35,7 +35,6 @@ class XList extends Action ->desc('List project variables') ->groups(['api', 'project']) ->label('scope', 'project.read') - ->label('resourceType', RESOURCE_TYPE_PROJECTS) ->label('sdk', new Method( namespace: 'project', group: 'variables', From 48fbb591133d123b7647368a0f807f5f5eb2530c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:44:47 +0200 Subject: [PATCH 49/73] pr review fixes --- app/controllers/general.php | 6 +++--- .../Platform/Modules/Functions/Http/Variables/Create.php | 2 +- .../Modules/Project/Http/Project/Variables/Create.php | 2 +- .../Platform/Modules/Sites/Http/Variables/Create.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 11cae2af88..21bcded22c 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -933,12 +933,12 @@ Http::init() */ $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { - if (version_compare($responseFormat, '1.9.3', '<')) { - $response->addFilter(new ResponseV24()); - } if (version_compare($responseFormat, '1.9.4', '<')) { $response->addFilter(new ResponseV25()); } + if (version_compare($responseFormat, '1.9.3', '<')) { + $response->addFilter(new ResponseV24()); + } if (version_compare($responseFormat, '1.9.2', '<')) { $response->addFilter(new ResponseV23()); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index e619e28928..de572cd41e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -91,7 +91,7 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php index f4e27def72..8c76ed2a8e 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php @@ -72,7 +72,7 @@ class Create extends Action QueueEvent $queueForEvents, Database $dbForProject, ) { - $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $variable = new Document([ '$id' => $variableId, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index da58f84f0b..edd3412b8f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -76,7 +76,7 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId; + $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId; $teamId = $project->getAttribute('teamId', ''); $variable = new Document([ From 15f21daa2bc7ba42db852478ff98491117d304b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:45:09 +0200 Subject: [PATCH 50/73] Add tests --- .../Functions/FunctionsConsoleClientTest.php | 303 ++++++++++++++++- .../Services/Sites/SitesCustomServerTest.php | 317 ++++++++++++++++++ 2 files changed, 619 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 06044d9984..eaf3f089e1 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -7,8 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; use Utopia\Console; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; class FunctionsConsoleClientTest extends Scope { @@ -70,6 +72,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -82,6 +85,7 @@ class FunctionsConsoleClientTest extends Scope $secretVariable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -196,6 +200,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -208,6 +213,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -226,6 +232,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'ANOTHERTESTINGVALUE', 'secret' => false @@ -234,10 +241,47 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(409, $variable['headers']['status-code']); + // Test for invalid variableId + $variable = $this->createVariable( + $functionId, + [ + 'variableId' => '!invalid-id!', + 'key' => 'INVALID_ID_KEY', + 'value' => 'value', + ] + ); + + $this->assertEquals(400, $variable['headers']['status-code']); + + // Test for duplicate variableId + $duplicateVariableId = ID::unique(); + $variable = $this->createVariable( + $functionId, + [ + 'variableId' => $duplicateVariableId, + 'key' => 'DUP_ID_KEY_1', + 'value' => 'value1', + ] + ); + + $this->assertEquals(201, $variable['headers']['status-code']); + + $duplicate = $this->createVariable( + $functionId, + [ + 'variableId' => $duplicateVariableId, + 'key' => 'DUP_ID_KEY_2', + 'value' => 'value2', + ] + ); + + $this->assertEquals(409, $duplicate['headers']['status-code']); + // Test for invalid key $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => str_repeat("A", 256), 'value' => 'TESTINGVALUE' ] @@ -249,6 +293,7 @@ class FunctionsConsoleClientTest extends Scope $variable = $this->createVariable( $functionId, [ + 'variableId' => ID::unique(), 'key' => 'LONGKEY', 'value' => str_repeat("#", 8193), ] @@ -283,6 +328,150 @@ class FunctionsConsoleClientTest extends Scope */ } + public function testListVariablesWithLimit(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables With Limit', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable1 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_1', + 'value' => 'limit-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_2', + 'value' => 'limit-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // List with limit of 1 + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['variables']); + $this->assertGreaterThanOrEqual(2, $response['body']['total']); + + $this->cleanupFunction($functionId); + } + + public function testListVariablesWithoutTotal(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables Without Total', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'NO_TOTAL_KEY', + 'value' => 'no-total-value', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // List with total=false + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'total' => false, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, $response['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($response['body']['variables'])); + + $this->cleanupFunction($functionId); + } + + public function testListVariablesCursorPagination(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test List Variables Cursor Pagination', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable1 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_1', + 'value' => 'cursor-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_2', + 'value' => 'cursor-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // Get first page with limit 1 + $page1 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page1['headers']['status-code']); + $this->assertCount(1, $page1['body']['variables']); + $cursorId = $page1['body']['variables'][0]['$id']; + + // Get next page using cursor + $page2 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page2['headers']['status-code']); + $this->assertCount(1, $page2['body']['variables']); + $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']); + + $this->cleanupFunction($functionId); + } + public function testGetVariable(): void { $data = $this->setupTestVariables(); @@ -337,6 +526,7 @@ class FunctionsConsoleClientTest extends Scope $functionId = $function['body']['$id']; $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -345,6 +535,7 @@ class FunctionsConsoleClientTest extends Scope $variableId = $variable['body']['$id']; $secretVariable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -457,6 +648,7 @@ class FunctionsConsoleClientTest extends Scope * Test for FAILURE */ + // Update with no parameters should fail with 400 $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -464,6 +656,7 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); + // Update with only value should succeed $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -471,7 +664,8 @@ class FunctionsConsoleClientTest extends Scope 'value' => 'TESTINGVALUEUPDATED_2' ]); - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("TESTINGVALUEUPDATED_2", $response['body']['value']); $longKey = str_repeat("A", 256); $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ @@ -496,6 +690,110 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); } + public function testUpdateVariableKey(): void + { + // Create a fresh function and variable for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Key', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'KEY_BEFORE', + 'value' => 'unchanged-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only key (key is nullable, but we provide a new key) + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'KEY_AFTER', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('KEY_AFTER', $response['body']['key']); + $this->assertEquals('unchanged-value', $response['body']['value']); + + $this->cleanupFunction($functionId); + } + + public function testUpdateVariableValueOnly(): void + { + // Create a fresh function and variable for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Value', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), + 'key' => 'UNCHANGED_KEY', + 'value' => 'value-before', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only value + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'value' => 'value-after', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('UNCHANGED_KEY', $response['body']['key']); + $this->assertEquals('value-after', $response['body']['value']); + + $this->cleanupFunction($functionId); + } + + public function testUpdateVariableNotFound(): void + { + // Create a fresh function for this test + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Update Variable Not Found', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/non-existent-id', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'NEW_KEY', + 'value' => 'new-value', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('variable_not_found', $response['body']['type']); + + $this->cleanupFunction($functionId); + } + public function testDeleteVariable(): void { // Create a fresh function and variables for this test since it deletes them @@ -512,6 +810,7 @@ class FunctionsConsoleClientTest extends Scope $functionId = $function['body']['$id']; $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST', 'value' => 'TESTINGVALUE', 'secret' => false @@ -520,6 +819,7 @@ class FunctionsConsoleClientTest extends Scope $variableId = $variable['body']['$id']; $secretVariable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'APP_TEST_1', 'value' => 'TESTINGVALUE_1', 'secret' => true @@ -585,6 +885,7 @@ class FunctionsConsoleClientTest extends Scope // create variable $variable = $this->createVariable($functionId, [ + 'variableId' => ID::unique(), 'key' => 'CUSTOM_VARIABLE', 'value' => 'a_secret_value', 'secret' => true, diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 7d9257c699..2beae74d3e 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -104,14 +104,17 @@ class SitesCustomServerTest extends Scope $this->assertEquals('./', $site['body']['outputDirectory']); $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', ]); $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', ]); $variable3 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', ]); @@ -211,6 +214,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('Test Site', $site['body']['name']); $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', 'secret' => false, @@ -223,6 +227,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(false, $variable['body']['secret']); $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', 'secret' => false, @@ -235,6 +240,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(false, $variable2['body']['secret']); $secretVariable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', 'secret' => true, @@ -330,6 +336,316 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + public function testListVariablesWithLimit(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables Limit', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable1 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_1', + 'value' => 'limit-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'LIMIT_KEY_2', + 'value' => 'limit-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // List with limit of 1 + $response = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['variables']); + $this->assertGreaterThanOrEqual(2, $response['body']['total']); + + $this->cleanupSite($siteId); + } + + public function testListVariablesWithoutTotal(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables No Total', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'NO_TOTAL_KEY', + 'value' => 'no-total-value', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // List with total=false + $response = $this->listVariables($siteId, [ + 'total' => false, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, $response['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($response['body']['variables'])); + + $this->cleanupSite($siteId); + } + + public function testListVariablesCursorPagination(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test List Variables Cursor', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable1 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_1', + 'value' => 'cursor-value-1', + ]); + $this->assertEquals(201, $variable1['headers']['status-code']); + + $variable2 = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'CURSOR_KEY_2', + 'value' => 'cursor-value-2', + ]); + $this->assertEquals(201, $variable2['headers']['status-code']); + + // Get first page with limit 1 + $page1 = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page1['headers']['status-code']); + $this->assertCount(1, $page1['body']['variables']); + $cursorId = $page1['body']['variables'][0]['$id']; + + // Get next page using cursor + $page2 = $this->listVariables($siteId, [ + 'queries' => [ + Query::limit(1)->toString(), + Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(), + ], + 'total' => true, + ]); + + $this->assertEquals(200, $page2['headers']['status-code']); + $this->assertCount(1, $page2['body']['variables']); + $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableKey(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Key', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'KEY_BEFORE', + 'value' => 'unchanged-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only key + $response = $this->updateVariable($siteId, $variableId, [ + 'key' => 'KEY_AFTER', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('KEY_AFTER', $response['body']['key']); + $this->assertEquals('unchanged-value', $response['body']['value']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableValueOnly(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Value', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'UNCHANGED_KEY', + 'value' => 'value-before', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update only value + $response = $this->updateVariable($siteId, $variableId, [ + 'value' => 'value-after', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('UNCHANGED_KEY', $response['body']['key']); + $this->assertEquals('value-after', $response['body']['value']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableNoOp(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable NoOp', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), + 'key' => 'NOOP_KEY', + 'value' => 'noop-value', + 'secret' => false + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + $variableId = $variable['body']['$id']; + + // Update with no parameters should fail with 400 + $response = $this->updateVariable($siteId, $variableId, []); + + $this->assertEquals(400, $response['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + public function testUpdateVariableNotFound(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Update Variable Not Found', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $response = $this->updateVariable($siteId, 'non-existent-id', [ + 'key' => 'NEW_KEY', + 'value' => 'new-value', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('variable_not_found', $response['body']['type']); + + $this->cleanupSite($siteId); + } + + public function testCreateVariableInvalidId(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Invalid Variable ID', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variable = $this->createVariable($siteId, [ + 'variableId' => '!invalid-id!', + 'key' => 'INVALID_ID_KEY', + 'value' => 'value', + ]); + + $this->assertEquals(400, $variable['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + public function testCreateVariableDuplicateId(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Duplicate Variable ID', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + $siteId = $site['body']['$id'] ?? ''; + $this->assertEquals(201, $site['headers']['status-code']); + + $variableId = ID::unique(); + + $variable = $this->createVariable($siteId, [ + 'variableId' => $variableId, + 'key' => 'DUP_ID_KEY_1', + 'value' => 'value1', + ]); + $this->assertEquals(201, $variable['headers']['status-code']); + + // Attempt to create with same ID + $duplicate = $this->createVariable($siteId, [ + 'variableId' => $variableId, + 'key' => 'DUP_ID_KEY_2', + 'value' => 'value2', + ]); + + $this->assertEquals(409, $duplicate['headers']['status-code']); + $this->assertEquals('variable_already_exists', $duplicate['body']['type']); + + $this->cleanupSite($siteId); + } + // This is first Sites test with Proxy // If this fails, it may not be related to variables; but Router flow failing public function testVariablesE2E(): void @@ -351,6 +667,7 @@ class SitesCustomServerTest extends Scope $domain = $this->setupSiteDomain($siteId); $secretVariable = $this->createVariable($siteId, [ + 'variableId' => ID::unique(), 'key' => 'name', 'value' => 'Appwrite', ]); From 1975ab251363e5d17e1bf5b1c54f5fdff387df36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:45:15 +0200 Subject: [PATCH 51/73] Fix linter --- src/Appwrite/Utopia/Response/Filters/V25.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php index 03d19d2cc1..3040ef681b 100644 --- a/src/Appwrite/Utopia/Response/Filters/V25.php +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -2,7 +2,6 @@ namespace Appwrite\Utopia\Response\Filters; -use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; // Convert 1.9.4 Data format to 1.9.3 format From 3e58500e13450c878e1d47b490aee71c5e718c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:51:57 +0200 Subject: [PATCH 52/73] Upgrade libs --- composer.json | 13 ++-- composer.lock | 165 ++++++++++++++++++++------------------------------ 2 files changed, 72 insertions(+), 106 deletions(-) diff --git a/composer.json b/composer.json index d6182ba750..b2b89fcee6 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "utopia-php/audit": "2.2.*", "utopia-php/auth": "0.5.*", "utopia-php/cache": "1.0.*", - "utopia-php/cli": "dev-feat-param-alias as 0.23.99", + "utopia-php/cli": "0.23.*", "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", @@ -67,7 +67,7 @@ "utopia-php/emails": "0.6.*", "utopia-php/dns": "1.6.*", "utopia-php/dsn": "0.2.1", - "utopia-php/http": "dev-feat-param-aliases as 0.34.99", + "utopia-php/http": "0.34.*", "utopia-php/fetch": "0.5.*", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", @@ -75,12 +75,12 @@ "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.9.*", - "utopia-php/platform": "dev-feat-param-aliases as 0.13.99", + "utopia-php/platform": "0.13.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "dev-feat-param-aliases as 0.17.99", - "utopia-php/servers": "dev-feat-param-aliases as 0.3.99", + "utopia-php/queue": "0.18.*", + "utopia-php/servers": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "2.*", "utopia-php/system": "0.10.*", @@ -117,6 +117,9 @@ "allow-plugins": { "php-http/discovery": true, "tbachert/spi": true + }, + "audit": { + "ignore": ["PKSA-sf9j-1gs7-xzvx"] } } } diff --git a/composer.lock b/composer.lock index 89ed556028..a92ecd0474 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ff46a826bc257b5ed2f0a8c821d8a0c4", + "content-hash": "842fa93d1bddb5eeef66ef09627ae84d", "packages": [ { "name": "adhocore/jwt", @@ -3658,21 +3658,21 @@ }, { "name": "utopia-php/cli", - "version": "dev-feat-param-alias", + "version": "0.23.3", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "c01a4af02249f20f5a9b3e23b6e8ee20f52857a2" + "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/c01a4af02249f20f5a9b3e23b6e8ee20f52857a2", - "reference": "c01a4af02249f20f5a9b3e23b6e8ee20f52857a2", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db", + "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/servers": "dev-feat-param-aliases" + "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/feat-param-alias" + "source": "https://github.com/utopia-php/cli/tree/0.23.3" }, - "time": "2026-05-04T12:39:30+00:00" + "time": "2026-05-05T04:38:59+00:00" }, { "name": "utopia-php/compression", @@ -4272,23 +4272,23 @@ }, { "name": "utopia-php/http", - "version": "dev-feat-param-aliases", + "version": "0.34.25", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "4439705c8c2e81af826f4b6d694469c9ebc84a9f" + "reference": "76be330d4197bae680eb4ccc29c573456fe91904" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/4439705c8c2e81af826f4b6d694469c9ebc84a9f", - "reference": "4439705c8c2e81af826f4b6d694469c9ebc84a9f", + "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": "dev-feat-param-aliases as 0.3.x-dev", + "utopia-php/servers": "0.4.0", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4322,9 +4322,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/feat-param-aliases" + "source": "https://github.com/utopia-php/http/tree/0.34.25" }, - "time": "2026-05-04T11:52:49+00:00" + "time": "2026-05-05T04:39:15+00:00" }, { "name": "utopia-php/image", @@ -4531,16 +4531,16 @@ }, { "name": "utopia-php/migration", - "version": "1.9.6", + "version": "1.9.7", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "b164631404ec759f8c368fe2321f44c22bc258ab" + "reference": "81b608a6871f56b70496803d12010823300aab6e" }, "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/81b608a6871f56b70496803d12010823300aab6e", + "reference": "81b608a6871f56b70496803d12010823300aab6e", "shasum": "" }, "require": { @@ -4580,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.9.7" }, - "time": "2026-04-30T08:11:07+00:00" + "time": "2026-05-05T07:18:48+00:00" }, { "name": "utopia-php/mongo", @@ -4647,26 +4647,26 @@ }, { "name": "utopia-php/platform", - "version": "dev-feat-param-aliases", + "version": "0.13.2", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "7fa008758c869671014ae4c4d52bb8cd686212f9" + "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/7fa008758c869671014ae4c4d52bb8cd686212f9", - "reference": "7fa008758c869671014ae4c4d52bb8cd686212f9", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9", + "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", "php": ">=8.3", - "utopia-php/cli": "dev-feat-param-alias as 0.23.99", - "utopia-php/http": "dev-feat-param-aliases as 0.34.99", - "utopia-php/queue": "dev-feat-param-aliases as 0.18.99", - "utopia-php/servers": "dev-feat-param-aliases as 0.3.99" + "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.*", @@ -4692,9 +4692,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/feat-param-aliases" + "source": "https://github.com/utopia-php/platform/tree/0.13.2" }, - "time": "2026-05-04T13:12:58+00:00" + "time": "2026-05-05T06:00:26+00:00" }, { "name": "utopia-php/pools", @@ -4804,16 +4804,16 @@ }, { "name": "utopia-php/queue", - "version": "dev-feat-param-aliases", + "version": "0.18.2", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "32b0c439a8fa4fc06b8b8793800a04be894bf03c" + "reference": "f85ca003c99ff475708c05466643d067403c0c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/32b0c439a8fa4fc06b8b8793800a04be894bf03c", - "reference": "32b0c439a8fa4fc06b8b8793800a04be894bf03c", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22", + "reference": "f85ca003c99ff475708c05466643d067403c0c22", "shasum": "" }, "require": { @@ -4821,7 +4821,7 @@ "php-amqplib/php-amqplib": "^3.7", "utopia-php/di": "0.3.*", "utopia-php/pools": "1.*", - "utopia-php/servers": "dev-feat-param-aliases as 0.3.99", + "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/feat-param-aliases" + "source": "https://github.com/utopia-php/queue/tree/0.18.2" }, - "time": "2026-05-04T12:59:54+00:00" + "time": "2026-05-05T04:38:59+00:00" }, { "name": "utopia-php/registry", @@ -4922,16 +4922,16 @@ }, { "name": "utopia-php/servers", - "version": "dev-feat-param-aliases", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa" + "reference": "7db346ef377503efe0acafe0791085270cd9ed70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa", - "reference": "24bbd8fbb6550bc72f2816d9b0c3b690e38e17fa", + "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/feat-param-aliases" + "source": "https://github.com/utopia-php/servers/tree/0.4.0" }, - "time": "2026-05-04T11:57:44+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", @@ -5465,16 +5465,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.26.0", + "version": "1.27.5", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "c14a7232ef0f22f594453730407c2719a2854973" + "reference": "9faa38b48d422f3da764a719712905c83b3922cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c14a7232ef0f22f594453730407c2719a2854973", - "reference": "c14a7232ef0f22f594453730407c2719a2854973", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb", + "reference": "9faa38b48d422f3da764a719712905c83b3922cb", "shasum": "" }, "require": { @@ -5510,9 +5510,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.26.0" + "source": "https://github.com/appwrite/sdk-generator/tree/1.27.5" }, - "time": "2026-05-04T09:46:22+00:00" + "time": "2026-05-05T12:09:40+00:00" }, { "name": "brianium/paratest", @@ -8442,46 +8442,9 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [ - { - "package": "utopia-php/cli", - "version": "dev-feat-param-alias", - "alias": "0.23.99", - "alias_normalized": "0.23.99.0" - }, - { - "package": "utopia-php/http", - "version": "dev-feat-param-aliases", - "alias": "0.34.99", - "alias_normalized": "0.34.99.0" - }, - { - "package": "utopia-php/platform", - "version": "dev-feat-param-aliases", - "alias": "0.13.99", - "alias_normalized": "0.13.99.0" - }, - { - "package": "utopia-php/queue", - "version": "dev-feat-param-aliases", - "alias": "0.17.99", - "alias_normalized": "0.17.99.0" - }, - { - "package": "utopia-php/servers", - "version": "dev-feat-param-aliases", - "alias": "0.3.99", - "alias_normalized": "0.3.99.0" - } - ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/cli": 20, - "utopia-php/http": 20, - "utopia-php/platform": 20, - "utopia-php/queue": 20, - "utopia-php/servers": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From b382a9c3e5412084101775de0b2cebfdb40643c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 16:53:24 +0200 Subject: [PATCH 53/73] Fix mistakes after AI --- composer.json | 3 --- composer.lock | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 693fe8bc47..76d16ef558 100644 --- a/composer.json +++ b/composer.json @@ -117,9 +117,6 @@ "allow-plugins": { "php-http/discovery": true, "tbachert/spi": true - }, - "audit": { - "ignore": ["PKSA-sf9j-1gs7-xzvx"] } } } diff --git a/composer.lock b/composer.lock index a92ecd0474..5f93f5b0a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "842fa93d1bddb5eeef66ef09627ae84d", + "content-hash": "17ccba478a5cace1251b2211e25021f2", "packages": [ { "name": "adhocore/jwt", @@ -5384,16 +5384,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v15.31.5", + "version": "v15.32.3", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9" + "reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9", - "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/993bf0bea17f870412ad8a90f60c41cb8d5f1145", + "reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145", "shasum": "" }, "require": { @@ -5402,16 +5402,16 @@ "php": "^7.4 || ^8" }, "require-dev": { - "amphp/amp": "^2.6", - "amphp/http-server": "^2.1", + "amphp/amp": "^2.6 || ^3", + "amphp/http-server": "^2.1 || ^3", "dms/phpunit-arraysubset-asserts": "dev-master", "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.94.2", + "friendsofphp/php-cs-fixer": "3.95.1", "mll-lab/php-cs-fixer-config": "5.13.0", "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "2.1.46", + "phpstan/phpstan": "2.1.51", "phpstan/phpstan-phpunit": "2.0.16", "phpstan/phpstan-strict-rules": "2.0.10", "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", @@ -5425,6 +5425,7 @@ "ticketswap/phpstan-error-formatter": "1.3.0" }, "suggest": { + "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)", "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", "psr/http-message": "To use standard GraphQL server", "react/promise": "To leverage async resolving on React PHP platform" @@ -5447,7 +5448,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.31.5" + "source": "https://github.com/webonyx/graphql-php/tree/v15.32.3" }, "funding": [ { @@ -5459,7 +5460,7 @@ "type": "open_collective" } ], - "time": "2026-04-11T18:06:15+00:00" + "time": "2026-04-24T13:49:35+00:00" } ], "packages-dev": [ From f90f618bffe07c8407a3bcd8df128fed6e9a4b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 5 May 2026 17:14:05 +0200 Subject: [PATCH 54/73] Fix failing tests --- src/Appwrite/Migration/Migration.php | 2 +- .../e2e/Services/Functions/FunctionsCustomServerTest.php | 9 +++++++++ .../e2e/Services/Projects/ProjectsConsoleClientTest.php | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 004e09cd23..08e32a9c74 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -96,7 +96,7 @@ abstract class Migration '1.9.1' => 'V24', '1.9.2' => 'V24', '1.9.3' => 'V24', - '1.9.4' => 'V25', + '1.9.4' => 'V24', ]; /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 899c0ff71f..f08b711fb2 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -53,14 +53,17 @@ class FunctionsCustomServerTest extends Scope $functionId = $function['body']['$id'] ?? ''; $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey1', 'value' => 'funcValue1', ]); $variable2 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey2', 'value' => 'funcValue2', ]); $variable3 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey3', 'value' => 'funcValue3', ]); @@ -109,6 +112,7 @@ class FunctionsCustomServerTest extends Scope // Create a variable for later tests $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); @@ -278,14 +282,17 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(10, $function['body']['timeout']); $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey1', 'value' => 'funcValue1', ]); $variable2 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey2', 'value' => 'funcValue2', ]); $variable3 = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'funcKey3', 'value' => 'funcValue3', ]); @@ -521,6 +528,7 @@ class FunctionsCustomServerTest extends Scope // Create a variable for later tests $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'GLOBAL_VARIABLE', 'value' => 'Global Variable Value', ]); @@ -2011,6 +2019,7 @@ class FunctionsCustomServerTest extends Scope ]); $variable = $this->createVariable($functionId, [ + 'variableId' => 'unique()', 'key' => 'CUSTOM_VARIABLE', 'value' => 'variable' ]); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 6936de9aff..3a9037c368 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -6813,6 +6813,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-mode' => 'admin', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ + 'variableId' => $variableId, 'key' => 'APP_TEST_' . $variableId, 'value' => 'TESTINGVALUE', 'secret' => false @@ -6832,6 +6833,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-mode' => 'admin', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ + 'variableId' => $variableId, 'key' => 'APP_TEST_' . $variableId, 'value' => 'TESTINGVALUE', 'secret' => false From 94c968e9414a9b37b9a6a11c36dfeb12a140f6ed Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 6 May 2026 04:35:29 +0000 Subject: [PATCH 55/73] feat(indexes): add 4 missing indexes (CLO-2333) - memberships: _key_team_confirm on (teamInternalId, confirm) for team-membership confirm-state queries - projects: _key_teamInternalId on teamInternalId for team-scoped project lookups - platforms: _key_project_id on projectId for user-facing-id lookups - webhooks: _key_project_id on projectId for user-facing-id lookups Re-applies the indexes from the stale PR #9629 (1.7.x base, conflicting) onto a fresh 1.9.x branch. None of these are in 1.9.x today; existing similar indexes target projectInternalId / teamId rather than the user-facing projectId / teamInternalId queries this addresses. --- app/config/collections/common.php | 7 +++++++ app/config/collections/platform.php | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 80bb717423..37fbcc8ca3 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1523,6 +1523,13 @@ return [ 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => ID::custom('_key_team_confirm'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['teamInternalId', 'confirm'], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 6195c11724..bb3413da5c 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -404,6 +404,13 @@ $platformCollections = [ 'lengths' => [], 'orders' => [], ], + [ + '$id' => ID::custom('_key_teamInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['teamInternalId'], + 'lengths' => [], + 'orders' => [], + ], ], ], @@ -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' => [], + 'orders' => [], + ], ], ], @@ -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' => [], + 'orders' => [], + ], ], ], From 83d56a2f368854c88a5644d09608a84ddcae3c7a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 6 May 2026 04:44:58 +0000 Subject: [PATCH 56/73] fix(indexes): set explicit lengths + orders on new indexes (greptile P1) All four new indexes left lengths/orders as empty arrays; greptile flagged the inconsistency vs every existing string-attribute index in the file (e.g. _key_team uses [LENGTH_KEY], _key_unique uses [LENGTH_KEY, LENGTH_KEY]). - memberships._key_team_confirm: [LENGTH_KEY, 0] for (string, boolean) + [ORDER_ASC, ORDER_ASC] - projects._key_teamInternalId: [LENGTH_KEY] + [ORDER_ASC] - platforms._key_project_id: [LENGTH_KEY] + [ORDER_ASC] - webhooks._key_project_id: [LENGTH_KEY] + [ORDER_ASC] --- app/config/collections/common.php | 4 ++-- app/config/collections/platform.php | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 37fbcc8ca3..8b5d623185 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1527,8 +1527,8 @@ return [ '$id' => ID::custom('_key_team_confirm'), 'type' => Database::INDEX_KEY, 'attributes' => ['teamInternalId', 'confirm'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [Database::LENGTH_KEY, 0], + 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], ], ], diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index bb3413da5c..748211f222 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -408,8 +408,8 @@ $platformCollections = [ '$id' => ID::custom('_key_teamInternalId'), 'type' => Database::INDEX_KEY, 'attributes' => ['teamInternalId'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], ], ], ], @@ -646,8 +646,8 @@ $platformCollections = [ '$id' => ID::custom('_key_project_id'), 'type' => Database::INDEX_KEY, 'attributes' => ['projectId'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], ], ], ], @@ -1026,8 +1026,8 @@ $platformCollections = [ '$id' => ID::custom('_key_project_id'), 'type' => Database::INDEX_KEY, 'attributes' => ['projectId'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], ], ], ], From c9ad685e11352a96d3b868ff2219e7bf07223cad Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 6 May 2026 04:48:53 +0000 Subject: [PATCH 57/73] fix(indexes): use empty lengths for mixed-type composite to match existing pattern greptile flagged missing lengths but the codebase has two conventions: - all-string composite: [LENGTH_KEY, LENGTH_KEY] (_key_unique, _key_provider_providerUid) - mixed/non-string composite: [] (e.g. ('enabled', 'type'), ('region', 'accessedAt'), ('targetInternalId', 'topicInternalId'), ('period', 'time'), ('metric', 'period', 'time')) (teamInternalId, confirm) is string+boolean; closest match is ('enabled', 'type') which uses lengths => [] and orders => []. Aligning to that established pattern instead of inventing [LENGTH_KEY, 0]. --- app/config/collections/common.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 8b5d623185..37fbcc8ca3 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1527,8 +1527,8 @@ return [ '$id' => ID::custom('_key_team_confirm'), 'type' => Database::INDEX_KEY, 'attributes' => ['teamInternalId', 'confirm'], - 'lengths' => [Database::LENGTH_KEY, 0], - 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], + 'lengths' => [], + 'orders' => [], ], ], ], From 03f7b62ff1a18a5adf3da1848af8d1eed1dbbe11 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 6 May 2026 11:24:22 +0530 Subject: [PATCH 58/73] refactor: update database version constraints and simplify bigint handling in collection creation Co-authored-by: Copilot --- composer.json | 4 +- composer.lock | 41 ++++++++----------- .../Http/Databases/Collections/Create.php | 20 +++------ 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/composer.json b/composer.json index f9c1072108..0138ad4f57 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/compression": "0.1.*", "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", - "utopia-php/database": "dev-big-init as 5.7", + "utopia-php/database": "5.*", "utopia-php/agents": "1.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", @@ -74,7 +74,7 @@ "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.*", diff --git a/composer.lock b/composer.lock index eb6212a620..9b88cedefc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "29610e3ef365af01d018077f2638ffb3", + "content-hash": "4440b62bc2eb25841157914c2b91b087", "packages": [ { "name": "adhocore/jwt", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "dev-big-init", + "version": "5.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ee8895a3ae835978fa4eb143619e4950e7001648" + "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ee8895a3ae835978fa4eb143619e4950e7001648", - "reference": "ee8895a3ae835978fa4eb143619e4950e7001648", + "url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511", + "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511", "shasum": "" }, "require": { @@ -3904,9 +3904,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/big-init" + "source": "https://github.com/utopia-php/database/tree/5.7.0" }, - "time": "2026-05-05T13:27:52+00:00" + "time": "2026-05-06T01:04:08+00:00" }, { "name": "utopia-php/detector", @@ -4531,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": { @@ -4549,7 +4549,7 @@ "ext-openssl": "*", "halaxa/json-machine": "^1.2", "php": ">=8.1", - "utopia-php/database": "dev-big-init as 5.4", + "utopia-php/database": "5.*", "utopia-php/dsn": "0.2.*", "utopia-php/storage": "2.*" }, @@ -4580,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", @@ -8444,18 +8444,9 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [ - { - "package": "utopia-php/database", - "version": "dev-big-init", - "alias": "5.7", - "alias_normalized": "5.7.0.0" - } - ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/database": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index 32c311b10a..3a53a49579 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -276,13 +276,7 @@ class Create extends Action ): array { $key = $attribute['key']; $type = $attribute['type']; - switch ($type) { - case Database::VAR_BIGINT: - $size = 8; - break; - default: - $size = $attribute['size'] ?? 0; - } + $size = $attribute['size'] ?? 0; $required = $attribute['required'] ?? false; $signed = $attribute['signed'] ?? true; $array = $attribute['array'] ?? false; @@ -296,13 +290,11 @@ class Create extends Action } if (isset($attribute['min']) || isset($attribute['max'])) { - if ($type === Database::VAR_INTEGER) { - $format = APP_DATABASE_ATTRIBUTE_INT_RANGE; - } elseif ($type === Database::VAR_BIGINT) { - $format = APP_DATABASE_ATTRIBUTE_BIGINT_RANGE; - } else { - $format = 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 || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX), From 93ce542d31b370208988015cb2beb0c429e9dc5d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 6 May 2026 11:37:05 +0530 Subject: [PATCH 59/73] updated composer --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 0138ad4f57..dac5be561f 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "appwrite/php-runtimes": "0.20.*", "appwrite/php-clamav": "2.0.*", "utopia-php/abuse": "1.2.*", + "utopia-php/agents": "1.2.*", "utopia-php/analytics": "0.15.*", "utopia-php/audit": "2.2.*", "utopia-php/auth": "0.5.*", @@ -61,7 +62,6 @@ "utopia-php/config": "1.*", "utopia-php/console": "0.1.*", "utopia-php/database": "5.*", - "utopia-php/agents": "1.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "1.*", "utopia-php/emails": "0.6.*", diff --git a/composer.lock b/composer.lock index 9b88cedefc..64c703cf2d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4440b62bc2eb25841157914c2b91b087", + "content-hash": "67c7d160c4a122b14eaa235d2417ea60", "packages": [ { "name": "adhocore/jwt", @@ -3403,16 +3403,16 @@ }, { "name": "utopia-php/agents", - "version": "1.3.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/utopia-php/agents.git", - "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30" + "reference": "052227953678a30ecc4b5467401fcb0b2386471e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30", - "reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e", + "reference": "052227953678a30ecc4b5467401fcb0b2386471e", "shasum": "" }, "require": { @@ -3450,9 +3450,9 @@ ], "support": { "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/1.3.0" + "source": "https://github.com/utopia-php/agents/tree/1.2.1" }, - "time": "2026-03-26T03:51:11+00:00" + "time": "2026-02-24T06:03:55+00:00" }, { "name": "utopia-php/analytics", From fc918d8b3c3945a8a1223e7a663c759e7960e71f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 6 May 2026 11:48:47 +0530 Subject: [PATCH 60/73] feat: add support for unsigned integer and bigint columns in database tests Co-authored-by: Copilot --- .../TablesDB/DatabasesNumericTypesTest.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php index 0e46d9466c..4280bfece9 100644 --- a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php +++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php @@ -80,6 +80,26 @@ class DatabasesNumericTypesTest extends Scope '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] = [ @@ -144,6 +164,22 @@ class DatabasesNumericTypesTest extends Scope '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 [ @@ -218,6 +254,45 @@ class DatabasesNumericTypesTest extends Scope $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(); @@ -237,6 +312,8 @@ class DatabasesNumericTypesTest extends Scope $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) { @@ -245,6 +322,8 @@ class DatabasesNumericTypesTest extends Scope $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 @@ -262,6 +341,8 @@ class DatabasesNumericTypesTest extends Scope 'data' => [ 'integer_field' => 5, 'bigint_field' => 456, + 'unsigned_int_field' => 50, + 'unsigned_bigint_field' => 9007199254740000, ], 'permissions' => [ Permission::read(Role::any()), @@ -271,6 +352,8 @@ class DatabasesNumericTypesTest extends Scope $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 From a286b78a0bf8a486319e7fef2fb4126ba3137d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 09:41:00 +0200 Subject: [PATCH 61/73] Fix function tests --- tests/e2e/Services/Functions/FunctionsConsoleClientTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index eaf3f089e1..5d501486fd 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -665,7 +665,6 @@ class FunctionsConsoleClientTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("TESTINGVALUEUPDATED_2", $response['body']['value']); $longKey = str_repeat("A", 256); $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ From 1ccf058c142313cd3292c710dfbeefdfbaac6565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 10:19:30 +0200 Subject: [PATCH 62/73] Fix failing tests --- app/init/constants.php | 2 +- tests/e2e/Services/GraphQL/FunctionsClientTest.php | 4 ++-- tests/e2e/Services/Migrations/MigrationsBase.php | 1 + .../e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index 64635bab2a..ccf446676e 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -44,7 +44,7 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 4324; +const APP_CACHE_BUSTER = 4325; const APP_VERSION_STABLE = '1.9.4'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php index ed436ad075..e8e033f353 100644 --- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php @@ -55,10 +55,10 @@ class FunctionsClientTest extends Scope $query = ' mutation createVariables($functionId: String!) { - var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") { + var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") { _id } - var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") { + var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") { _id } } diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 4346e5a5fa..2621240900 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1096,6 +1096,7 @@ trait MigrationsBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-appwrite-response-format' => '1.9.3' ], [ 'key' => 'TEST_VAR', 'value' => 'test_value', diff --git a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php index 9085733b70..fa369cefc9 100644 --- a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php @@ -89,6 +89,7 @@ class WebhooksCustomServerTest extends Scope $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', ], $this->getHeaders()), [ 'key' => 'key1', 'value' => 'value1', From d27b5788889b92bf3110241616e1b8da3e32eab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 10:34:37 +0200 Subject: [PATCH 63/73] Fix more tests --- tests/e2e/Services/GraphQL/FunctionsServerTest.php | 4 ++-- .../e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index 572fde49bf..95b52bcbe3 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -55,10 +55,10 @@ class FunctionsServerTest extends Scope $query = ' mutation createVariables($functionId: String!) { - var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") { + var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") { _id } - var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") { + var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") { _id } } diff --git a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php index fa369cefc9..eb08da56f2 100644 --- a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php @@ -700,6 +700,7 @@ class WebhooksCustomServerTest extends Scope $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', ], $this->getHeaders()), [ 'key' => 'key1', 'value' => 'value1', From 00ee9c6b0e697e6567581bc26ce82e144181141b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 11:09:11 +0200 Subject: [PATCH 64/73] Fix benchmark --- tests/benchmarks/http.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index f7bb54024d..ef42e99663 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -380,6 +380,7 @@ function computeFlow(ctx) { api('GET', '/functions/runtimes', null, ctx.sessionHeaders, [200], 'functions.runtimes.list'); api('GET', '/functions/specifications', null, ctx.apiHeaders, [200], 'functions.specifications.list'); const functionVariable = api('POST', `/functions/${functionId}/variables`, { + variableId: 'unique()', key: 'BENCHMARK', value: 'true', secret: false, From dcef7ef559ebf1f51604b8b2d2db3e9c165cfac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 11:24:53 +0200 Subject: [PATCH 65/73] OIDC param name improvement --- .../Http/Project/OAuth2/Oidc/Update.php | 22 ++--- src/Appwrite/Utopia/Response/Filters/V25.php | 17 ++++ .../Utopia/Response/Model/OAuth2Oidc.php | 4 +- tests/e2e/Services/Project/OAuth2Base.php | 92 +++++++++---------- 4 files changed, 76 insertions(+), 59 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 9598ff4c43..697b306be8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -82,13 +82,13 @@ class Update extends Base 'hint' => '', ], [ - '$id' => 'tokenUrl', + '$id' => 'tokenURL', 'name' => 'Token URL', 'example' => 'https://myoauth.com/oauth2/token', 'hint' => '', ], [ - '$id' => 'userInfoUrl', + '$id' => 'userInfoURL', 'name' => 'User Info URL', 'example' => 'https://myoauth.com/oauth2/userinfo', 'hint' => '', @@ -127,8 +127,8 @@ class Update extends Base ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) ->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) ->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) - ->param('tokenUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) - ->param('userInfoUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true) + ->param('tokenURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true, aliases: ['tokenUrl']) + ->param('userInfoURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true, aliases: ['userInfoUrl']) ->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true) ->inject('response') ->inject('dbForPlatform') @@ -151,8 +151,8 @@ class Update extends Base static::getClientSecretParamName() => '', 'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '', 'authorizationURL' => $decoded['authorizationEndpoint'] ?? '', - 'tokenUrl' => $decoded['tokenEndpoint'] ?? '', - 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '', + 'tokenURL' => $decoded['tokenEndpoint'] ?? '', + 'userInfoURL' => $decoded['userInfoEndpoint'] ?? '', ]); } @@ -174,8 +174,8 @@ class Update extends Base ?string $clientSecret, ?string $wellKnownURL, ?string $authorizationURL, - ?string $tokenUrl, - ?string $userInfoUrl, + ?string $tokenURL, + ?string $userInfoURL, ?bool $enabled, Response $response, Database $dbForPlatform, @@ -201,8 +201,8 @@ class Update extends Base 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), 'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''), 'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''), - 'tokenEndpoint' => $tokenUrl ?? ($existing['tokenEndpoint'] ?? ''), - 'userInfoEndpoint' => $userInfoUrl ?? ($existing['userInfoEndpoint'] ?? ''), + 'tokenEndpoint' => $tokenURL ?? ($existing['tokenEndpoint'] ?? ''), + 'userInfoEndpoint' => $userInfoURL ?? ($existing['userInfoEndpoint'] ?? ''), ]; // When enabling, require either wellKnownEndpoint alone, or all three @@ -215,7 +215,7 @@ class Update extends Base && !empty($merged['userInfoEndpoint']); if (!$hasWellKnown && !$hasAllDiscovery) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenUrl, and userInfoUrl.'); + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenURL, and userInfoURL.'); } } diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php index 3040ef681b..e0ce76c724 100644 --- a/src/Appwrite/Utopia/Response/Filters/V25.php +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -2,6 +2,7 @@ namespace Appwrite\Utopia\Response\Filters; +use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; // Convert 1.9.4 Data format to 1.9.3 format @@ -10,7 +11,23 @@ class V25 extends Filter public function parse(array $content, string $model): array { return match ($model) { + Response::MODEL_OAUTH2_OIDC => $this->parseOAuth2Oidc($content), default => $content, }; } + + private function parseOAuth2Oidc(array $content): array + { + if (isset($content['tokenURL'])) { + $content['tokenUrl'] = $content['tokenURL']; + unset($content['tokenURL']); + } + + if (isset($content['userInfoURL'])) { + $content['userInfoUrl'] = $content['userInfoURL']; + unset($content['userInfoURL']); + } + + return $content; + } } diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php index e4f0919666..0b18539423 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php @@ -42,13 +42,13 @@ class OAuth2Oidc extends OAuth2Base 'default' => '', 'example' => 'https://myoauth.com/oauth2/authorize', ]) - ->addRule('tokenUrl', [ + ->addRule('tokenURL', [ 'type' => self::TYPE_STRING, 'description' => 'OpenID Connect token endpoint URL.', 'default' => '', 'example' => 'https://myoauth.com/oauth2/token', ]) - ->addRule('userInfoUrl', [ + ->addRule('userInfoURL', [ 'type' => self::TYPE_STRING, 'description' => 'OpenID Connect user info endpoint URL.', 'default' => '', diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index a55cc559b2..5a3fa764f6 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -1651,8 +1651,8 @@ trait OAuth2Base $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']); $this->assertArrayHasKey('authorizationURL', $response['body']); - $this->assertArrayHasKey('tokenUrl', $response['body']); - $this->assertArrayHasKey('userInfoUrl', $response['body']); + $this->assertArrayHasKey('tokenURL', $response['body']); + $this->assertArrayHasKey('userInfoURL', $response['body']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1660,8 +1660,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1672,15 +1672,15 @@ trait OAuth2Base 'clientId' => 'oidc-discovery', 'clientSecret' => 'oidc-discovery-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => false, ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1688,8 +1688,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1701,8 +1701,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1731,8 +1731,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1740,7 +1740,7 @@ trait OAuth2Base 'clientId' => 'oidc-partial', 'clientSecret' => 'oidc-partial-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'tokenURL' => 'https://idp.example.com/oauth2/token', 'enabled' => true, ]); @@ -1753,8 +1753,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1785,8 +1785,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1816,8 +1816,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1831,8 +1831,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1841,7 +1841,7 @@ trait OAuth2Base 'clientId' => 'oidc-split-discovery', 'clientSecret' => 'oidc-split-discovery-secret', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'tokenURL' => 'https://idp.example.com/oauth2/token', 'enabled' => false, ]); @@ -1849,19 +1849,19 @@ trait OAuth2Base // state must include the two stored URLs + the new one to satisfy // the all-three-discovery-URLs branch of the enable check. $enable = $this->updateOAuth2('oidc', [ - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => true, ]); $this->assertSame(200, $enable['headers']['status-code']); $this->assertTrue($enable['body']['enabled']); // Confirm all three URLs ended up persisted (merge wrote the new - // userInfoUrl while preserving the previously stored two). + // userInfoURL while preserving the previously stored two). $get = $this->getOAuth2Provider('oidc'); $this->assertSame(200, $get['headers']['status-code']); $this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1869,8 +1869,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1883,8 +1883,8 @@ trait OAuth2Base 'clientSecret' => 'oidc-clear-then-enable-secret', 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); @@ -1907,8 +1907,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1929,16 +1929,16 @@ trait OAuth2Base $switch = $this->updateOAuth2('oidc', [ 'wellKnownURL' => '', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => true, ]); $this->assertSame(200, $switch['headers']['status-code']); $this->assertTrue($switch['body']['enabled']); $this->assertSame('', $switch['body']['wellKnownURL']); $this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']); - $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']); - $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']); + $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ @@ -1946,8 +1946,8 @@ trait OAuth2Base 'clientSecret' => '', 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', 'enabled' => false, ]); } @@ -1961,23 +1961,23 @@ trait OAuth2Base 'clientSecret' => 'oidc-clear-secret', 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'tokenURL' => 'https://idp.example.com/oauth2/token', + 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo', 'enabled' => false, ]); $response = $this->updateOAuth2('oidc', [ 'wellKnownURL' => '', 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', + 'tokenURL' => '', + 'userInfoURL' => '', ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('', $response['body']['wellKnownURL']); $this->assertSame('', $response['body']['authorizationURL']); - $this->assertSame('', $response['body']['tokenUrl']); - $this->assertSame('', $response['body']['userInfoUrl']); + $this->assertSame('', $response['body']['tokenURL']); + $this->assertSame('', $response['body']['userInfoURL']); // Cleanup $this->updateOAuth2('oidc', [ From 389146c6256bb48106e9e2aaa3f15d44e2309273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 11:32:00 +0200 Subject: [PATCH 66/73] oidc backwards compatibiltiy test --- tests/e2e/Services/Project/OAuth2Base.php | 90 +++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5a3fa764f6..47d92a2a58 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -1987,6 +1987,96 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void + { + // Reset to clean state + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenURL' => '', + 'userInfoURL' => '', + 'enabled' => false, + ]); + + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.3', + ]; + $headers = \array_merge($headers, $this->getHeaders()); + + // Update using OLD param names (aliases must still work) + $response = $this->client->call( + Client::METHOD_PATCH, + '/project/oauth2/oidc', + $headers, + [ + 'clientId' => 'oidc-compat-client', + 'clientSecret' => 'oidc-compat-secret', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => false, + ], + ); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('tokenUrl', $response['body']); + $this->assertArrayHasKey('userInfoUrl', $response['body']); + $this->assertArrayNotHasKey('tokenURL', $response['body']); + $this->assertArrayNotHasKey('userInfoURL', $response['body']); + $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']); + + // GET with 1.9.3 format must also return old param names + $get = $this->client->call( + Client::METHOD_GET, + '/project/oauth2/oidc', + $headers, + ); + + $this->assertSame(200, $get['headers']['status-code']); + $this->assertArrayHasKey('tokenUrl', $get['body']); + $this->assertArrayHasKey('userInfoUrl', $get['body']); + $this->assertArrayNotHasKey('tokenURL', $get['body']); + $this->assertArrayNotHasKey('userInfoURL', $get['body']); + $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']); + + // LIST with 1.9.3 format must also return old param names for OIDC + $list = $this->client->call( + Client::METHOD_GET, + '/project/oauth2', + $headers, + ); + + $this->assertSame(200, $list['headers']['status-code']); + $oidcEntry = null; + foreach ($list['body']['providers'] as $provider) { + if ($provider['$id'] === 'oidc') { + $oidcEntry = $provider; + break; + } + } + $this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response'); + $this->assertArrayHasKey('tokenUrl', $oidcEntry); + $this->assertArrayHasKey('userInfoUrl', $oidcEntry); + $this->assertArrayNotHasKey('tokenURL', $oidcEntry); + $this->assertArrayNotHasKey('userInfoURL', $oidcEntry); + $this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'tokenURL' => '', + 'userInfoURL' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Okta (clientId + clientSecret + optional domain/authServer) // ========================================================================= From 140cbb633d008c3d1e3d66adeaa264b5027bb2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 11:45:35 +0200 Subject: [PATCH 67/73] Fix tests --- src/Appwrite/Utopia/Response/Filters/V25.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php index e0ce76c724..e445843b51 100644 --- a/src/Appwrite/Utopia/Response/Filters/V25.php +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -12,6 +12,7 @@ class V25 extends Filter { return match ($model) { Response::MODEL_OAUTH2_OIDC => $this->parseOAuth2Oidc($content), + Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => $this->parseOAuth2Oidc($item)), default => $content, }; } From a65c6136f45cec9f3fc1d85a36159a47e2781709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 12:08:02 +0200 Subject: [PATCH 68/73] Improve code quality --- src/Appwrite/Utopia/Response/Filters/V25.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php index e445843b51..bda98ed0d8 100644 --- a/src/Appwrite/Utopia/Response/Filters/V25.php +++ b/src/Appwrite/Utopia/Response/Filters/V25.php @@ -12,7 +12,7 @@ class V25 extends Filter { return match ($model) { Response::MODEL_OAUTH2_OIDC => $this->parseOAuth2Oidc($content), - Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => $this->parseOAuth2Oidc($item)), + Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => ($item['$id'] ?? null) === 'oidc' ? $this->parseOAuth2Oidc($item) : $item), default => $content, }; } From d2b551cd122bbce9388c3a6e298f82403cd9386b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 15:50:18 +0200 Subject: [PATCH 69/73] Fix refreshing nonoauth sessions --- app/controllers/api/account.php | 4 +- .../Account/AccountCustomClientTest.php | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c6a5fd6f97..a3060c16c5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -830,11 +830,11 @@ Http::patch('/v1/account/sessions/:sessionId') $refreshToken = $session->getAttribute('providerRefreshToken', ''); $oAuthProviders = Config::getParam('oAuthProviders') ?? []; $className = $oAuthProviders[$provider]['class'] ?? null; - if (!empty($provider) && ($className === null || !\class_exists($className))) { + if (!empty($refreshToken) && ($className === null || !\class_exists($className))) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } - if (!empty($provider) && \class_exists($className)) { + if (\class_exists($className)) { $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index da788c3caa..671937e704 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4163,4 +4163,72 @@ class AccountCustomClientTest extends Scope $this->assertEquals(401, $verification3['headers']['status-code']); } + + public function testRefreshEmailPasswordSession(): void + { + $email = uniqid() . 'user@localhost.test'; + + $account = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => 'password', + ]); + + $this->assertEquals(201, $account['headers']['status-code']); + + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'password', + ]); + + $this->assertEquals(201, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['$id']); + + $sessionId = $session['body']['$id']; + $cookie = 'a_session_' . $this->getProject()['$id'] . '=' .$session['cookies']['a_session_' . $this->getProject()['$id']]; + + $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['expire']); + $expiryBefore = $session['body']['expire']; + + \sleep(3); // Small delay to ensure expiry an expand + + $session = $this->client->call(Client::METHOD_PATCH, '/account/sessions/' . $sessionId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertNotEmpty($session['body']['expire']); + $expiryAfter = $session['body']['expire']; + + $this->assertGreaterThan($expiryAfter, $expiryBefore); + + $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie, + ])); + + $this->assertEquals(200, $session['headers']['status-code']); + $this->assertEquals($expiryAfter, $session['body']['expire']); + } } From a9dd957a7affca5730af3dd10b82ec3f7da09442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 15:57:25 +0200 Subject: [PATCH 70/73] Fix test --- tests/e2e/Services/Account/AccountCustomClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 671937e704..0fe0f31b4f 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4229,6 +4229,6 @@ class AccountCustomClientTest extends Scope ])); $this->assertEquals(200, $session['headers']['status-code']); - $this->assertEquals($expiryAfter, $session['body']['expire']); + $this->assertEquals(\strtotime($expiryAfter), \strtotime($session['body']['expire'])); } } From e834a95213de38cc97f2ed12640089d2c6c42f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 May 2026 16:21:50 +0200 Subject: [PATCH 71/73] PR review improvements --- app/controllers/api/account.php | 2 +- tests/e2e/Services/Account/AccountCustomClientTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a3060c16c5..65fd0bfb83 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -834,7 +834,7 @@ Http::patch('/v1/account/sessions/:sessionId') throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } - if (\class_exists($className)) { + if ($className !== null && \class_exists($className)) { $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 0fe0f31b4f..160ee39e21 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4219,7 +4219,7 @@ class AccountCustomClientTest extends Scope $this->assertNotEmpty($session['body']['expire']); $expiryAfter = $session['body']['expire']; - $this->assertGreaterThan($expiryAfter, $expiryBefore); + $this->assertGreaterThan(\strtotime($expiryBefore), \strtotime($expiryAfter)); $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([ 'origin' => 'http://localhost', From 8201fea9efab00598d2fa710cb73ceac1d469ca0 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 6 May 2026 15:50:36 +0100 Subject: [PATCH 72/73] Differentiate executor timeouts for builds, sync, and async executions Adds Executor\Exception\Timeout (with timeoutSeconds) and translates it at each call site into BUILD_TIMEOUT, FUNCTION_SYNCHRONOUS_TIMEOUT, or FUNCTION_ASYNCHRONOUS_TIMEOUT instead of always using the misleading sync function error. Build timeouts now append to streamed buildLogs rather than replacing them, and the build worker reports its timeout via Span. --- app/config/errors.php | 10 +++++ app/controllers/general.php | 45 ++++++++++--------- src/Appwrite/Extend/Exception.php | 2 + .../Functions/Http/Executions/Create.php | 43 ++++++++++-------- .../Modules/Functions/Workers/Builds.php | 20 ++++++--- src/Appwrite/Platform/Workers/Functions.php | 41 +++++++++-------- src/Executor/Exception.php | 7 +++ src/Executor/Exception/Timeout.php | 23 ++++++++++ src/Executor/Executor.php | 18 ++++---- 9 files changed, 136 insertions(+), 73 deletions(-) create mode 100644 src/Executor/Exception.php create mode 100644 src/Executor/Exception/Timeout.php diff --git a/app/config/errors.php b/app/config/errors.php index fa112bcb6f..0293be3212 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -623,6 +623,11 @@ return [ 'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.', 'code' => 408, ], + Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT => [ + 'name' => Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT, + 'description' => 'Asynchronous function execution timed out. Ensure the execution duration doesn\'t exceed the configured function timeout.', + 'code' => 408, + ], Exception::FUNCTION_TEMPLATE_NOT_FOUND => [ 'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND, 'description' => 'Function Template with the requested ID could not be found.', @@ -687,6 +692,11 @@ return [ 'description' => 'Build with the requested ID failed. Please check the logs for more information.', 'code' => 400, ], + Exception::BUILD_TIMEOUT => [ + 'name' => Exception::BUILD_TIMEOUT, + 'description' => 'Build timed out. Increase the build timeout via the `_APP_COMPUTE_BUILD_TIMEOUT` environment variable, or simplify the build to complete within the limit.', + 'code' => 408, + ], /** Deployments */ Exception::DEPLOYMENT_NOT_FOUND => [ diff --git a/app/controllers/general.php b/app/controllers/general.php index 21bcded22c..bc63d200d7 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -41,6 +41,7 @@ use Appwrite\Utopia\Response\Filters\V23 as ResponseV23; use Appwrite\Utopia\Response\Filters\V24 as ResponseV24; use Appwrite\Utopia\Response\Filters\V25 as ResponseV25; use Appwrite\Utopia\View; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use MaxMind\Db\Reader; use Swoole\Http\Request as SwooleRequest; @@ -581,26 +582,30 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S 'site' => '', }; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $resource->getAttribute('timeout', 30), - image: $runtime['image'], - source: $source, - entrypoint: $entrypoint, - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $runtimeEntrypoint, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $resource->getAttribute('logging', true), - requestTimeout: 30, - responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $resource->getAttribute('timeout', 30), + image: $runtime['image'], + source: $source, + entrypoint: $entrypoint, + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $runtimeEntrypoint, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $resource->getAttribute('logging', true), + requestTimeout: 30, + responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th); + } $headerOverrides = []; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 6fc3e88635..2cf8e79944 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -178,6 +178,7 @@ class Exception extends \Exception public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing'; public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; + public const string FUNCTION_ASYNCHRONOUS_TIMEOUT = 'function_asynchronous_timeout'; public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing'; @@ -192,6 +193,7 @@ class Exception extends \Exception public const string BUILD_ALREADY_COMPLETED = 'build_already_completed'; public const string BUILD_CANCELED = 'build_canceled'; public const string BUILD_FAILED = 'build_failed'; + public const string BUILD_TIMEOUT = 'build_timeout'; /** Execution */ public const string EXECUTION_NOT_FOUND = 'execution_not_found'; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 9f15cf9d1e..02dd76294e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -17,6 +17,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Usage\Context; use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use MaxMind\Db\Reader; use Utopia\Auth\Proofs\Token; @@ -417,25 +418,29 @@ class Create extends Base $source = $deployment->getAttribute('buildPath', ''); $extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz'; $command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\""; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $source, - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $source, + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + requestTimeout: 30 + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th); + } $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 352fb56e28..d285cf3a36 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -14,7 +14,9 @@ use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Usage\Context; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; +use Appwrite\Extend\Exception as AppwriteException; use Exception; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use Swoole\Coroutine as Co; use Utopia\Cache\Cache; @@ -34,6 +36,7 @@ use Utopia\Detector\Detector\Rendering; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Span\Span; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; @@ -183,6 +186,8 @@ class Builds extends Action array $platform, int $timeout ): void { + Span::add('timeout', $timeout); + Console::info('Deployment action started'); $startTime = DateTime::now(); @@ -720,6 +725,9 @@ class Builds extends Action ); Console::log('createRuntime finished'); + } catch (ExecutorTimeout $error) { + Console::warning('createRuntime timed out'); + $err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error); } catch (\Throwable $error) { Console::warning('createRuntime failed'); $err = $error; @@ -1147,13 +1155,11 @@ class Builds extends Action $message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_START}', '', $message); $message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_END}', '', $message); - // Combine with previous logs if deployment got past build process - $previousLogs = ''; - if (! is_null($deployment->getAttribute('buildSize', null))) { - $previousLogs = $deployment->getAttribute('buildLogs', ''); - if (! empty($previousLogs)) { - $message = $previousLogs . "\n" . $message; - } + // Append error to whatever build logs were already streamed + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $previousLogs = $deployment->getAttribute('buildLogs', ''); + if (! empty($previousLogs)) { + $message = $previousLogs . "\n" . $message; } $endTime = DateTime::now(); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 8167fb975d..a72b16cc23 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -10,6 +10,7 @@ use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Utopia\Response\Model\Execution; +use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; use Utopia\Bus\Bus; use Utopia\Config\Config; @@ -565,24 +566,28 @@ class Functions extends Action Span::add('trigger', $trigger); Span::current()?->finish(); } - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deploymentId, - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $source, - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - ); + try { + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deploymentId, + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $source, + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + ); + } catch (ExecutorTimeout $th) { + throw new AppwriteException(AppwriteException::FUNCTION_ASYNCHRONOUS_TIMEOUT, previous: $th); + } $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; diff --git a/src/Executor/Exception.php b/src/Executor/Exception.php new file mode 100644 index 0000000000..b799d22567 --- /dev/null +++ b/src/Executor/Exception.php @@ -0,0 +1,7 @@ +timeoutSeconds; + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index eb74867c9c..c570970732 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -2,9 +2,9 @@ namespace Executor; -use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Utopia\Fetch\BodyMultipart; -use Exception; +use Executor\Exception as ExecutorException; +use Executor\Exception\Timeout as ExecutorTimeout; use Utopia\System\System; class Executor @@ -104,7 +104,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -163,7 +163,7 @@ class Executor } if ($status >= 400) { - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -247,7 +247,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } $headers = $response['body']['headers'] ?? []; @@ -281,7 +281,7 @@ class Executor $status = $response['headers']['status-code']; if ($status >= 400) { $message = \is_string($response['body']) ? $response['body'] : $response['body']['message']; - throw new \Exception($message, $status); + throw new ExecutorException($message, $status); } return $response['body']; @@ -401,7 +401,7 @@ class Executor $json = json_decode($responseBody, true); if ($json === null) { - throw new Exception('Failed to parse response: ' . $responseBody); + throw new ExecutorException('Failed to parse response: ' . $responseBody); } $responseBody = $json; @@ -412,9 +412,9 @@ class Executor if ($curlError) { if ($curlError == CURLE_OPERATION_TIMEDOUT) { - throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT); + throw new ExecutorTimeout('Executor request timed out', $timeout); } - throw new Exception($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus); + throw new ExecutorException($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus); } $responseHeaders['status-code'] = $responseStatus; From 9387c480f50cd699c282a05612d61e9db43556bd Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 6 May 2026 19:56:17 +0100 Subject: [PATCH 73/73] Add Span attributes to build worker for trace observability --- .../Platform/Modules/Functions/Workers/Builds.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index d285cf3a36..a0bd37732f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -10,11 +10,11 @@ use Appwrite\Event\Publisher\Screenshot; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; +use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Usage\Context; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; -use Appwrite\Extend\Exception as AppwriteException; use Exception; use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; @@ -186,6 +186,10 @@ class Builds extends Action array $platform, int $timeout ): void { + Span::add('projectId', $project->getId()); + Span::add('resourceId', $resource->getId()); + Span::add('resourceType', $resource->getCollection()); + Span::add('deploymentId', $deployment->getId()); Span::add('timeout', $timeout); Console::info('Deployment action started'); @@ -228,8 +232,12 @@ class Builds extends Action $version = $this->getVersion($resource); $runtime = $this->getRuntime($resource, $version); + Span::add('runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', '')); + Span::add('version', $version); $spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + Span::add('cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)); + Span::add('memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT)); // Realtime preparation $event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";