mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into presence-api
This commit is contained in:
@@ -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' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -404,6 +404,13 @@ $platformCollections = [
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_teamInternalId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['teamInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -635,6 +642,13 @@ $platformCollections = [
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_project_id'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1007,7 +1021,14 @@ $platformCollections = [
|
||||
'attributes' => ['projectInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_project_id'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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 => [
|
||||
|
||||
@@ -63,8 +63,8 @@ $admins = [
|
||||
'oauth2.write',
|
||||
'mocks.read',
|
||||
'mocks.write',
|
||||
'policies.read',
|
||||
'policies.write',
|
||||
'project.policies.read',
|
||||
'project.policies.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
'projects.write',
|
||||
|
||||
@@ -44,11 +44,23 @@ return [
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control",
|
||||
"category" => "Project",
|
||||
'deprecated' => true,
|
||||
],
|
||||
"policies.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control",
|
||||
"category" => "Project",
|
||||
'deprecated' => true,
|
||||
],
|
||||
"project.policies.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s policies",
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.write" => [
|
||||
"project.policies.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s policies",
|
||||
"category" => "Project",
|
||||
@@ -286,6 +298,16 @@ return [
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
|
||||
// Proxy
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read proxy rules.',
|
||||
'category' => 'Proxy',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete proxy rules.',
|
||||
'category' => 'Proxy',
|
||||
],
|
||||
|
||||
// Other
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
@@ -339,14 +361,6 @@ return [
|
||||
'description' => 'Access to create, update, and delete resources under VCS service.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'presences.read' => [
|
||||
'description' => 'Access to read your project\'s presences',
|
||||
],
|
||||
|
||||
@@ -830,11 +830,11 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
$refreshToken = $session->getAttribute('providerRefreshToken', '');
|
||||
$oAuthProviders = Config::getParam('oAuthProviders') ?? [];
|
||||
$className = $oAuthProviders[$provider]['class'] ?? null;
|
||||
if (!empty($provider) && ($className === null || !\class_exists($className))) {
|
||||
if (!empty($refreshToken) && ($className === null || !\class_exists($className))) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
if (!empty($provider) && \class_exists($className)) {
|
||||
if ($className !== null && \class_exists($className)) {
|
||||
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
||||
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
|
||||
|
||||
+33
-20
@@ -28,6 +28,7 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
|
||||
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
|
||||
use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -38,7 +39,9 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
|
||||
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
|
||||
use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Exception\Timeout as ExecutorTimeout;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
@@ -579,26 +582,30 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
'site' => '',
|
||||
};
|
||||
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $resource->getAttribute('timeout', 30),
|
||||
image: $runtime['image'],
|
||||
source: $source,
|
||||
entrypoint: $entrypoint,
|
||||
version: $version,
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $runtimeEntrypoint,
|
||||
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
|
||||
logging: $resource->getAttribute('logging', true),
|
||||
requestTimeout: 30,
|
||||
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
|
||||
);
|
||||
try {
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $resource->getAttribute('timeout', 30),
|
||||
image: $runtime['image'],
|
||||
source: $source,
|
||||
entrypoint: $entrypoint,
|
||||
version: $version,
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $runtimeEntrypoint,
|
||||
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
|
||||
logging: $resource->getAttribute('logging', true),
|
||||
requestTimeout: 30,
|
||||
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
|
||||
);
|
||||
} catch (ExecutorTimeout $th) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
|
||||
}
|
||||
|
||||
$headerOverrides = [];
|
||||
|
||||
@@ -904,6 +911,9 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.3', '<')) {
|
||||
$request->addFilter(new RequestV24());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.4', '<')) {
|
||||
$request->addFilter(new RequestV25());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -928,6 +938,9 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.4', '<')) {
|
||||
$response->addFilter(new ResponseV25());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.3', '<')) {
|
||||
$response->addFilter(new ResponseV24());
|
||||
}
|
||||
|
||||
@@ -44,14 +44,15 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4324;
|
||||
const APP_VERSION_STABLE = '1.9.3';
|
||||
const APP_CACHE_BUSTER = 4325;
|
||||
const APP_VERSION_STABLE = '1.9.4';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime';
|
||||
const APP_DATABASE_ATTRIBUTE_URL = 'url';
|
||||
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
|
||||
const APP_DATABASE_ATTRIBUTE_BIGINT_RANGE = 'bigintRange';
|
||||
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
|
||||
const APP_DATABASE_ATTRIBUTE_POINT = 'point';
|
||||
const APP_DATABASE_ATTRIBUTE_LINE = 'line';
|
||||
|
||||
@@ -36,6 +36,13 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
|
||||
return new Range($min, $max, Range::TYPE_INTEGER);
|
||||
}, Database::VAR_INTEGER);
|
||||
|
||||
// BigInt uses a dedicated bigintRange format name to avoid clobbering `intRange`.
|
||||
Structure::addFormat(APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, function ($attribute) {
|
||||
$min = $attribute['formatOptions']['min'] ?? -INF;
|
||||
$max = $attribute['formatOptions']['max'] ?? INF;
|
||||
return new Range($min, $max, Range::TYPE_INTEGER);
|
||||
}, Database::VAR_BIGINT);
|
||||
|
||||
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
|
||||
$min = $attribute['formatOptions']['min'] ?? -INF;
|
||||
$max = $attribute['formatOptions']['max'] ?? INF;
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Response\Model\AlgoScryptModified;
|
||||
use Appwrite\Utopia\Response\Model\AlgoSha;
|
||||
use Appwrite\Utopia\Response\Model\Any;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
use Appwrite\Utopia\Response\Model\AttributeBigInt;
|
||||
use Appwrite\Utopia\Response\Model\AttributeBoolean;
|
||||
use Appwrite\Utopia\Response\Model\AttributeDatetime;
|
||||
use Appwrite\Utopia\Response\Model\AttributeEmail;
|
||||
@@ -37,6 +38,7 @@ use Appwrite\Utopia\Response\Model\Branch;
|
||||
use Appwrite\Utopia\Response\Model\Bucket;
|
||||
use Appwrite\Utopia\Response\Model\Collection;
|
||||
use Appwrite\Utopia\Response\Model\Column;
|
||||
use Appwrite\Utopia\Response\Model\ColumnBigInt;
|
||||
use Appwrite\Utopia\Response\Model\ColumnBoolean;
|
||||
use Appwrite\Utopia\Response\Model\ColumnDatetime;
|
||||
use Appwrite\Utopia\Response\Model\ColumnEmail;
|
||||
@@ -300,6 +302,7 @@ Response::setModel(new Attribute());
|
||||
Response::setModel(new AttributeList());
|
||||
Response::setModel(new AttributeString());
|
||||
Response::setModel(new AttributeInteger());
|
||||
Response::setModel(new AttributeBigInt());
|
||||
Response::setModel(new AttributeFloat());
|
||||
Response::setModel(new AttributeBoolean());
|
||||
Response::setModel(new AttributeEmail());
|
||||
@@ -333,6 +336,7 @@ Response::setModel(new Column());
|
||||
Response::setModel(new ColumnList());
|
||||
Response::setModel(new ColumnString());
|
||||
Response::setModel(new ColumnInteger());
|
||||
Response::setModel(new ColumnBigInt());
|
||||
Response::setModel(new ColumnFloat());
|
||||
Response::setModel(new ColumnBoolean());
|
||||
Response::setModel(new ColumnEmail());
|
||||
|
||||
+3
-3
@@ -74,13 +74,13 @@
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/migration": "1.*",
|
||||
"utopia-php/platform": "0.13.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/queue": "0.18.*",
|
||||
"utopia-php/servers": "0.4.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "2.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
|
||||
Generated
+88
-88
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2af4d953af2a624be8bf2f89ea27336c",
|
||||
"content-hash": "ec2ad489c60f0102f0dfab223b6d1fe4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2708,16 +2708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.8",
|
||||
"version": "v7.4.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342"
|
||||
"reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6",
|
||||
"reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2785,7 +2785,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.8"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.9"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T12:55:43+00:00"
|
||||
"time": "2026-04-29T13:25:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
@@ -3658,21 +3658,21 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cli",
|
||||
"version": "0.23.2",
|
||||
"version": "0.23.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/cli.git",
|
||||
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab"
|
||||
"reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
|
||||
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
|
||||
"reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"utopia-php/servers": "0.3.*"
|
||||
"utopia-php/servers": "0.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
@@ -3703,9 +3703,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/cli/issues",
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.23.2"
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.23.3"
|
||||
},
|
||||
"time": "2026-04-27T09:19:04+00:00"
|
||||
"time": "2026-05-05T04:38:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/compression",
|
||||
@@ -3850,22 +3850,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.4.2",
|
||||
"version": "5.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "a1bb3e2a4fba13032ea625b21a21039c43cffeda"
|
||||
"reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/a1bb3e2a4fba13032ea625b21a21039c43cffeda",
|
||||
"reference": "a1bb3e2a4fba13032ea625b21a21039c43cffeda",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
|
||||
"reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-mongodb": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.4",
|
||||
"utopia-php/cache": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
@@ -3903,9 +3904,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.4.2"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.7.0"
|
||||
},
|
||||
"time": "2026-04-30T09:59:57+00:00"
|
||||
"time": "2026-05-06T01:04:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4271,23 +4272,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.24",
|
||||
"version": "0.34.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
|
||||
"reference": "76be330d4197bae680eb4ccc29c573456fe91904"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/76be330d4197bae680eb4ccc29c573456fe91904",
|
||||
"reference": "76be330d4197bae680eb4ccc29c573456fe91904",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/servers": "0.4.0",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
@@ -4321,9 +4322,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.24"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.25"
|
||||
},
|
||||
"time": "2026-04-24T12:16:53+00:00"
|
||||
"time": "2026-05-05T04:39:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4530,16 +4531,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.9.5",
|
||||
"version": "1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599"
|
||||
"reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/55f4863d690e775f44fec3cae4bd1f4491fed5ea",
|
||||
"reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4579,9 +4580,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.5"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.10.0"
|
||||
},
|
||||
"time": "2026-04-29T11:19:13+00:00"
|
||||
"time": "2026-05-06T04:35:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -4646,26 +4647,26 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
|
||||
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
|
||||
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.1",
|
||||
"utopia-php/cli": "0.23.*",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*"
|
||||
"php": ">=8.3",
|
||||
"utopia-php/cli": "0.23.3",
|
||||
"utopia-php/http": "0.34.25",
|
||||
"utopia-php/queue": "0.18.2",
|
||||
"utopia-php/servers": "0.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
@@ -4691,9 +4692,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.13.2"
|
||||
},
|
||||
"time": "2026-04-17T09:57:18+00:00"
|
||||
"time": "2026-05-05T06:00:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -4803,25 +4804,24 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/queue",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/queue.git",
|
||||
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
|
||||
"reference": "f85ca003c99ff475708c05466643d067403c0c22"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
|
||||
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22",
|
||||
"reference": "f85ca003c99ff475708c05466643d067403c0c22",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"php-amqplib/php-amqplib": "^3.7",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/servers": "0.4.0",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
@@ -4864,9 +4864,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/queue/issues",
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.17.0"
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.18.2"
|
||||
},
|
||||
"time": "2026-03-23T16:21:31+00:00"
|
||||
"time": "2026-05-05T04:38:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/registry",
|
||||
@@ -4922,16 +4922,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/servers",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/servers.git",
|
||||
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
|
||||
"reference": "7db346ef377503efe0acafe0791085270cd9ed70"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
|
||||
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70",
|
||||
"reference": "7db346ef377503efe0acafe0791085270cd9ed70",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4970,9 +4970,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/servers/issues",
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.3.0"
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.4.0"
|
||||
},
|
||||
"time": "2026-03-13T11:31:42+00:00"
|
||||
"time": "2026-05-05T04:08:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/span",
|
||||
@@ -5020,16 +5020,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/storage",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/storage.git",
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e"
|
||||
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/64e132a3768e22243eda36fe4262da22fd204f3c",
|
||||
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5066,22 +5066,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/storage/issues",
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.1"
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.2"
|
||||
},
|
||||
"time": "2026-04-29T09:05:48+00:00"
|
||||
"time": "2026-05-01T15:06:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/system.git",
|
||||
"reference": "7c1669533bb9c285de19191270c8c1439161a78a"
|
||||
"reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a",
|
||||
"reference": "7c1669533bb9c285de19191270c8c1439161a78a",
|
||||
"url": "https://api.github.com/repos/utopia-php/system/zipball/04229a822b147c1abaf1a92fb42c2d7aad4625df",
|
||||
"reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5122,9 +5122,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/system/issues",
|
||||
"source": "https://github.com/utopia-php/system/tree/0.10.1"
|
||||
"source": "https://github.com/utopia-php/system/tree/0.10.2"
|
||||
},
|
||||
"time": "2026-03-15T21:07:41+00:00"
|
||||
"time": "2026-05-05T14:33:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/telemetry",
|
||||
@@ -5466,16 +5466,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.25.1",
|
||||
"version": "1.27.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade"
|
||||
"reference": "9faa38b48d422f3da764a719712905c83b3922cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb",
|
||||
"reference": "9faa38b48d422f3da764a719712905c83b3922cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5511,9 +5511,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.25.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.27.5"
|
||||
},
|
||||
"time": "2026-04-28T11:12:22+00:00"
|
||||
"time": "2026-05-05T12:09:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6620,16 +6620,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.23",
|
||||
"version": "12.5.24",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969"
|
||||
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046",
|
||||
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6698,7 +6698,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6706,7 +6706,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-18T06:12:49+00:00"
|
||||
"time": "2026-05-01T04:21:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -7691,16 +7691,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
|
||||
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d",
|
||||
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7757,7 +7757,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.9"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7777,7 +7777,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-04-29T15:02:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Create a bigint attribute. Optionally, minimum and maximum values can be provided.
|
||||
@@ -0,0 +1 @@
|
||||
Update a bigint attribute. Changing the `default` value will not update already existing documents.
|
||||
@@ -0,0 +1 @@
|
||||
Create a bigint column. Optionally, minimum and maximum values can be provided.
|
||||
@@ -0,0 +1 @@
|
||||
Update a bigint column. Changing the `default` value will not update already existing rows.
|
||||
@@ -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';
|
||||
|
||||
+9
-3
@@ -241,6 +241,10 @@ abstract class Action extends UtopiaAction
|
||||
? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER
|
||||
: UtopiaResponse::MODEL_COLUMN_INTEGER,
|
||||
|
||||
Database::VAR_BIGINT => $isCollections
|
||||
? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT
|
||||
: UtopiaResponse::MODEL_COLUMN_BIGINT,
|
||||
|
||||
Database::VAR_FLOAT => $isCollections
|
||||
? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT
|
||||
: UtopiaResponse::MODEL_COLUMN_FLOAT,
|
||||
@@ -540,6 +544,7 @@ abstract class Action extends UtopiaAction
|
||||
|
||||
switch ($attribute->getAttribute('format')) {
|
||||
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
|
||||
case APP_DATABASE_ATTRIBUTE_BIGINT_RANGE:
|
||||
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
|
||||
$min ??= $attribute->getAttribute('formatOptions')['min'];
|
||||
$max ??= $attribute->getAttribute('formatOptions')['max'];
|
||||
@@ -548,14 +553,15 @@ abstract class Action extends UtopiaAction
|
||||
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
|
||||
}
|
||||
|
||||
if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) {
|
||||
$validator = new Range($min, $max, Database::VAR_INTEGER);
|
||||
} else {
|
||||
if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_FLOAT_RANGE) {
|
||||
$validator = new Range($min, $max, Database::VAR_FLOAT);
|
||||
|
||||
if (!is_null($default)) {
|
||||
$default = \floatval($default);
|
||||
}
|
||||
} else {
|
||||
// intRange and bigintRange share the same integer range semantics
|
||||
$validator = new Range($min, $max, Range::TYPE_INTEGER);
|
||||
}
|
||||
|
||||
if (!is_null($default) && !$validator->isValid($default)) {
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
|
||||
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'createBigIntAttribute';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string|array
|
||||
{
|
||||
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint')
|
||||
->desc('Create bigint attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', 'collections.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/create-bigint-attribute.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_ACCEPTED,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'tablesDB.createBigIntColumn',
|
||||
),
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
|
||||
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
|
||||
->param('required', null, new Boolean(), 'Is attribute required?')
|
||||
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
|
||||
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
|
||||
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.', true)
|
||||
->param('array', false, new Boolean(), 'Is attribute an array?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
|
||||
{
|
||||
$min ??= \PHP_INT_MIN;
|
||||
$max ??= \PHP_INT_MAX;
|
||||
|
||||
if ($min > $max) {
|
||||
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
|
||||
}
|
||||
|
||||
$validator = new Range($min, $max, Range::TYPE_INTEGER);
|
||||
if (!\is_null($default) && !$validator->isValid($default)) {
|
||||
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
|
||||
}
|
||||
|
||||
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
|
||||
'key' => $key,
|
||||
'type' => Database::VAR_BIGINT,
|
||||
'size' => 8,
|
||||
'required' => $required,
|
||||
'default' => $default,
|
||||
'array' => $array,
|
||||
'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
|
||||
'formatOptions' => ['min' => $min, 'max' => $max],
|
||||
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
|
||||
|
||||
$formatOptions = $attribute->getAttribute('formatOptions', []);
|
||||
if (!empty($formatOptions)) {
|
||||
$attribute->setAttribute('min', \intval($formatOptions['min']));
|
||||
$attribute->setAttribute('max', \intval($formatOptions['max']));
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($attribute, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'updateBigIntAttribute';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string|array
|
||||
{
|
||||
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint/:key')
|
||||
->desc('Update bigint attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', 'collections.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/update-bigint-attribute.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'tablesDB.updateBigIntColumn',
|
||||
),
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
|
||||
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
|
||||
->param('required', null, new Boolean(), 'Is attribute required?')
|
||||
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
|
||||
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
|
||||
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.')
|
||||
->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
|
||||
{
|
||||
$attribute = $this->updateAttribute(
|
||||
databaseId: $databaseId,
|
||||
collectionId: $collectionId,
|
||||
key: $key,
|
||||
dbForProject: $dbForProject,
|
||||
queueForEvents: $queueForEvents,
|
||||
authorization: $authorization,
|
||||
type: Database::VAR_BIGINT,
|
||||
default: $default,
|
||||
required: $required,
|
||||
min: $min,
|
||||
max: $max,
|
||||
newKey: $newKey
|
||||
);
|
||||
|
||||
$formatOptions = $attribute->getAttribute('formatOptions', []);
|
||||
if (!empty($formatOptions)) {
|
||||
$attribute->setAttribute('min', \intval($formatOptions['min']));
|
||||
$attribute->setAttribute('max', \intval($formatOptions['max']));
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($attribute, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
@@ -290,13 +290,15 @@ class Create extends Action
|
||||
}
|
||||
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$format = $type === Database::VAR_INTEGER
|
||||
? APP_DATABASE_ATTRIBUTE_INT_RANGE
|
||||
: APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
|
||||
$format = match($type) {
|
||||
Database::VAR_INTEGER => APP_DATABASE_ATTRIBUTE_INT_RANGE,
|
||||
Database::VAR_BIGINT => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
|
||||
default => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
|
||||
};
|
||||
|
||||
$formatOptions = [
|
||||
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
|
||||
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
|
||||
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
|
||||
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as BigIntCreate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class Create extends BigIntCreate
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'createBigIntColumn';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string|array
|
||||
{
|
||||
return UtopiaResponse::MODEL_COLUMN_BIGINT;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint')
|
||||
->desc('Create bigint column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/create-bigint-column.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_ACCEPTED,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
|
||||
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
|
||||
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
|
||||
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.', true)
|
||||
->param('array', false, new Boolean(), 'Is column an array?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as BigIntUpdate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class Update extends BigIntUpdate
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'updateBigIntColumn';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string|array
|
||||
{
|
||||
return UtopiaResponse::MODEL_COLUMN_BIGINT;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key')
|
||||
->desc('Update bigint column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/tablesdb/update-bigint-column.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
|
||||
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
|
||||
->param('required', null, new Boolean(), 'Is column required?')
|
||||
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
|
||||
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
|
||||
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.')
|
||||
->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Services\Registry;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as CreateBigIntAttribute;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as UpdateBigIntAttribute;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as CreateBooleanAttribute;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as UpdateBooleanAttribute;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as CreateDatetimeAttribute;
|
||||
@@ -171,6 +173,10 @@ class Legacy extends Base
|
||||
$service->addAction(CreateIntegerAttribute::getName(), new CreateIntegerAttribute());
|
||||
$service->addAction(UpdateIntegerAttribute::getName(), new UpdateIntegerAttribute());
|
||||
|
||||
// Attribute: BigInt
|
||||
$service->addAction(CreateBigIntAttribute::getName(), new CreateBigIntAttribute());
|
||||
$service->addAction(UpdateBigIntAttribute::getName(), new UpdateBigIntAttribute());
|
||||
|
||||
// Attribute: IP
|
||||
$service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute());
|
||||
$service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute());
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Create as CreateTablesDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Delete as DeleteTablesDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Get as GetTablesDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Create as CreateBigInt;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Update as UpdateBigInt;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Create as CreateBoolean;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Update as UpdateBoolean;
|
||||
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Datetime\Create as CreateDatetime;
|
||||
@@ -151,6 +153,10 @@ class TablesDB extends Base
|
||||
$service->addAction(CreateInteger::getName(), new CreateInteger());
|
||||
$service->addAction(UpdateInteger::getName(), new UpdateInteger());
|
||||
|
||||
// Column: BigInt
|
||||
$service->addAction(CreateBigInt::getName(), new CreateBigInt());
|
||||
$service->addAction(UpdateBigInt::getName(), new UpdateBigInt());
|
||||
|
||||
// Column: IP
|
||||
$service->addAction(CreateIP::getName(), new CreateIP());
|
||||
$service->addAction(UpdateIP::getName(), new UpdateIP());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -38,6 +40,7 @@ class Create extends Base
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.write')
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('event', 'variables.[variableId].create')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('audits.resource', 'function/{request.functionId}')
|
||||
->label('sdk', new Method(
|
||||
@@ -56,10 +59,12 @@ class Create extends Base
|
||||
]
|
||||
))
|
||||
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
@@ -69,10 +74,12 @@ class Create extends Base
|
||||
|
||||
public function action(
|
||||
string $functionId,
|
||||
string $variableId,
|
||||
string $key,
|
||||
string $value,
|
||||
bool $secret,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
@@ -84,7 +91,7 @@ class Create extends Base
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variableId = ID::unique();
|
||||
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$variable = new Document([
|
||||
@@ -120,6 +127,8 @@ class Create extends Base
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
|
||||
$queueForEvents->setParam('variableId', $variable->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> $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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ use Appwrite\Event\Publisher\Screenshot;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
use Appwrite\Vcs\Comment;
|
||||
use Exception;
|
||||
use Executor\Exception\Timeout as ExecutorTimeout;
|
||||
use Executor\Executor;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -34,6 +36,7 @@ use Utopia\Detector\Detector\Rendering;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\System\System;
|
||||
@@ -183,6 +186,12 @@ class Builds extends Action
|
||||
array $platform,
|
||||
int $timeout
|
||||
): void {
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('resourceId', $resource->getId());
|
||||
Span::add('resourceType', $resource->getCollection());
|
||||
Span::add('deploymentId', $deployment->getId());
|
||||
Span::add('timeout', $timeout);
|
||||
|
||||
Console::info('Deployment action started');
|
||||
|
||||
$startTime = DateTime::now();
|
||||
@@ -223,8 +232,12 @@ class Builds extends Action
|
||||
|
||||
$version = $this->getVersion($resource);
|
||||
$runtime = $this->getRuntime($resource, $version);
|
||||
Span::add('runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', ''));
|
||||
Span::add('version', $version);
|
||||
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
Span::add('cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
|
||||
Span::add('memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT));
|
||||
|
||||
// Realtime preparation
|
||||
$event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";
|
||||
@@ -720,6 +733,9 @@ class Builds extends Action
|
||||
);
|
||||
|
||||
Console::log('createRuntime finished');
|
||||
} catch (ExecutorTimeout $error) {
|
||||
Console::warning('createRuntime timed out');
|
||||
$err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error);
|
||||
} catch (\Throwable $error) {
|
||||
Console::warning('createRuntime failed');
|
||||
$err = $error;
|
||||
@@ -1147,13 +1163,11 @@ class Builds extends Action
|
||||
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_START}', '', $message);
|
||||
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_END}', '', $message);
|
||||
|
||||
// Combine with previous logs if deployment got past build process
|
||||
$previousLogs = '';
|
||||
if (! is_null($deployment->getAttribute('buildSize', null))) {
|
||||
$previousLogs = $deployment->getAttribute('buildLogs', '');
|
||||
if (! empty($previousLogs)) {
|
||||
$message = $previousLogs . "\n" . $message;
|
||||
}
|
||||
// Append error to whatever build logs were already streamed
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$previousLogs = $deployment->getAttribute('buildLogs', '');
|
||||
if (! empty($previousLogs)) {
|
||||
$message = $previousLogs . "\n" . $message;
|
||||
}
|
||||
|
||||
$endTime = DateTime::now();
|
||||
|
||||
@@ -59,7 +59,7 @@ class Create extends Base
|
||||
],
|
||||
))
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
|
||||
->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false)
|
||||
->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
|
||||
@@ -11,7 +11,7 @@ use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
@@ -86,28 +86,28 @@ class Get extends Action
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.')
|
||||
->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $provider,
|
||||
string $providerId,
|
||||
Response $response,
|
||||
Document $project,
|
||||
): void {
|
||||
$providers = Config::getParam('oAuthProviders', []);
|
||||
if (!\array_key_exists($provider, $providers) || !($providers[$provider]['enabled'] ?? false)) {
|
||||
if (!\array_key_exists($providerId, $providers) || !($providers[$providerId]['enabled'] ?? false)) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$actions = Base::getProviderActions();
|
||||
if (!isset($actions[$provider])) {
|
||||
if (!isset($actions[$providerId])) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$updateClass = $actions[$provider];
|
||||
$updateClass = $actions[$providerId];
|
||||
$action = new $updateClass();
|
||||
|
||||
$response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel());
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
@@ -43,15 +50,28 @@ class XList extends Action
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $queries
|
||||
*/
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Document $project,
|
||||
): void {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$providers = Config::getParam('oAuthProviders', []);
|
||||
$actions = Base::getProviderActions();
|
||||
|
||||
@@ -66,8 +86,16 @@ class XList extends Action
|
||||
$documents[] = $action->buildReadResponse($project);
|
||||
}
|
||||
|
||||
$total = $includeTotal ? \count($documents) : 0;
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
$limit = $grouped['limit'] ?? null;
|
||||
|
||||
$documents = \array_slice($documents, $offset, $limit);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => \count($documents),
|
||||
'total' => $total,
|
||||
'providers' => $documents,
|
||||
]), Response::MODEL_OAUTH2_PROVIDER_LIST);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
@@ -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}')
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
+1
-1
@@ -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}')
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -53,7 +53,7 @@ class Create extends Action
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.')
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.')
|
||||
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
@@ -72,7 +72,7 @@ class Create extends Action
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForProject,
|
||||
) {
|
||||
$variableId = ($variableId == 'unique()') ? ID::unique() : $variableId;
|
||||
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
|
||||
|
||||
$variable = new Document([
|
||||
'$id' => $variableId,
|
||||
|
||||
@@ -51,7 +51,7 @@ class Delete extends Action
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
|
||||
@@ -44,7 +44,7 @@ class Get extends Action
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -52,7 +52,7 @@ class Update extends Action
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
|
||||
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
|
||||
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
@@ -43,12 +44,14 @@ class Create extends Action
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'createAPIRule',
|
||||
description: <<<EOT
|
||||
Create a new proxy rule for serving Appwrite's API on custom domain.
|
||||
|
||||
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
@@ -67,11 +70,21 @@ class Create extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('platform')
|
||||
->inject('log')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
|
||||
{
|
||||
public function action(
|
||||
string $domain,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Certificate $publisherForCertificates,
|
||||
Event $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
array $platform,
|
||||
Log $log,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
@@ -108,7 +121,7 @@ class Create extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$rule = $dbForPlatform->createDocument('rules', $rule);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
|
||||
} catch (Duplicate $e) {
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -126,6 +139,13 @@ class Create extends Action
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
|
||||
@@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -38,12 +39,12 @@ class Delete extends Action
|
||||
->label('audits.resource', 'rule/{request.ruleId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'deleteRule',
|
||||
description: <<<EOT
|
||||
Delete a proxy rule by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
@@ -58,6 +59,7 @@ class Delete extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -67,15 +69,16 @@ class Delete extends Action
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
DeleteEvent $queueForDeletes,
|
||||
Event $queueForEvents
|
||||
Event $queueForEvents,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$rule = $dbForPlatform->getDocument('rules', $ruleId);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('rules', $rule->getId());
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId()));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -45,12 +46,14 @@ class Create extends Action
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'createFunctionRule',
|
||||
description: <<<EOT
|
||||
Create a new proxy rule for executing Appwrite Function on custom domain.
|
||||
|
||||
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
@@ -72,11 +75,25 @@ class Create extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('platform')
|
||||
->inject('log')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
public function action(
|
||||
string $domain,
|
||||
string $functionId,
|
||||
string $branch,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Certificate $publisherForCertificates,
|
||||
Event $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Database $dbForProject,
|
||||
array $platform,
|
||||
Log $log,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
@@ -126,7 +143,7 @@ class Create extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$rule = $dbForPlatform->createDocument('rules', $rule);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
|
||||
} catch (Duplicate $e) {
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -144,6 +161,13 @@ class Create extends Action
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
|
||||
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
@@ -34,12 +35,12 @@ class Get extends Action
|
||||
->label('scope', 'rules.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'getRule',
|
||||
description: <<<EOT
|
||||
Get a proxy rule by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
@@ -51,6 +52,7 @@ class Get extends Action
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -58,15 +60,16 @@ class Get extends Action
|
||||
string $ruleId,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Database $dbForPlatform
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$rule = $dbForPlatform->getDocument('rules', $ruleId);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
|
||||
$certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
|
||||
|
||||
// Give priority to certificate generation logs if present
|
||||
if (!empty($certificate->getAttribute('logs', ''))) {
|
||||
@@ -75,6 +78,13 @@ class Get extends Action
|
||||
|
||||
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -46,12 +47,14 @@ class Create extends Action
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'createRedirectRule',
|
||||
description: <<<EOT
|
||||
Create a new proxy rule for to redirect from custom domain to another domain.
|
||||
|
||||
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
@@ -75,11 +78,27 @@ class Create extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('platform')
|
||||
->inject('log')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
public function action(
|
||||
string $domain,
|
||||
string $url,
|
||||
int $statusCode,
|
||||
string $resourceId,
|
||||
string $resourceType,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Certificate $publisherForCertificates,
|
||||
Event $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Database $dbForProject,
|
||||
array $platform,
|
||||
Log $log,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
$collection = match ($resourceType) {
|
||||
@@ -131,7 +150,7 @@ class Create extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$rule = $dbForPlatform->createDocument('rules', $rule);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
|
||||
} catch (Duplicate $e) {
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -149,6 +168,13 @@ class Create extends Action
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -45,12 +46,14 @@ class Create extends Action
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'createSiteRule',
|
||||
description: <<<EOT
|
||||
Create a new proxy rule for serving Appwrite Site on custom domain.
|
||||
|
||||
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
@@ -72,11 +75,25 @@ class Create extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('platform')
|
||||
->inject('log')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
public function action(
|
||||
string $domain,
|
||||
string $siteId,
|
||||
?string $branch,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Certificate $publisherForCertificates,
|
||||
Event $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Database $dbForProject,
|
||||
array $platform,
|
||||
Log $log,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
@@ -126,7 +143,7 @@ class Create extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$rule = $dbForPlatform->createDocument('rules', $rule);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
|
||||
} catch (Duplicate $e) {
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -144,6 +161,13 @@ class Create extends Action
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
|
||||
+19
-15
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Status;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
@@ -13,6 +13,7 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -32,8 +33,9 @@ class Update extends Action
|
||||
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/proxy/rules/:ruleId/verification')
|
||||
->desc('Update rule verification status')
|
||||
->setHttpPath('/v1/proxy/rules/:ruleId/status')
|
||||
->httpAlias('/v1/proxy/rules/:ruleId/verification')
|
||||
->desc('Update rule status')
|
||||
->groups(['api', 'proxy'])
|
||||
->label('scope', 'rules.write')
|
||||
->label('event', 'rules.[ruleId].update')
|
||||
@@ -41,12 +43,12 @@ class Update extends Action
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
name: 'updateRuleVerification',
|
||||
group: 'rules',
|
||||
name: 'updateRuleStatus',
|
||||
description: <<<EOT
|
||||
Retry getting verification process of a proxy rule. This endpoint triggers domain verification by checking DNS records (CNAME) against the configured target domain. If verification is successful, a TLS certificate will be automatically provisioned for the domain.
|
||||
If not succeeded yet, retry verification process of a proxy rule domain. This endpoint triggers domain verification by checking DNS records. If verification is successful, a TLS certificate will be automatically provisioned for the domain asynchronously in the background.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
@@ -61,6 +63,7 @@ class Update extends Action
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('log')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -71,9 +74,10 @@ class Update extends Action
|
||||
Event $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Log $log
|
||||
Log $log,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$rule = $dbForPlatform->getDocument('rules', $ruleId);
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
@@ -90,22 +94,22 @@ class Update extends Action
|
||||
try {
|
||||
$this->verifyRule($rule, $log);
|
||||
// Reset logs and status for the rule
|
||||
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
'logs' => '',
|
||||
'status' => RULE_STATUS_CERTIFICATE_GENERATING,
|
||||
]));
|
||||
])));
|
||||
|
||||
$certificateId = $rule->getAttribute('certificateId', '');
|
||||
// Reset logs for the associated certificate.
|
||||
if (!empty($certificateId)) {
|
||||
$certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
|
||||
$certificate = $authorization->skip(fn () => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
|
||||
'logs' => '',
|
||||
]));
|
||||
])));
|
||||
}
|
||||
} catch (Exception $err) {
|
||||
$dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
'$updatedAt' => DateTime::now(),
|
||||
]));
|
||||
])));
|
||||
throw $err;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
@@ -39,12 +40,12 @@ class XList extends Action
|
||||
->label('scope', 'rules.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
group: 'rules',
|
||||
name: 'listRules',
|
||||
description: <<<EOT
|
||||
Get a list of all the proxy rules. You can use the query params to filter your results.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
@@ -53,21 +54,23 @@ class XList extends Action
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true, deprecated: true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $total,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Database $dbForPlatform
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
@@ -91,7 +94,7 @@ class XList extends Action
|
||||
}
|
||||
|
||||
$ruleId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
|
||||
@@ -102,9 +105,9 @@ class XList extends Action
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$rules = $dbForPlatform->find('rules', $queries);
|
||||
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
|
||||
foreach ($rules as $rule) {
|
||||
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
|
||||
$certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
|
||||
|
||||
// Give priority to certificate generation logs if present
|
||||
if (!empty($certificate->getAttribute('logs', ''))) {
|
||||
@@ -112,11 +115,18 @@ class XList extends Action
|
||||
}
|
||||
|
||||
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
|
||||
|
||||
// Rename 'created' status to 'unverified' for consistency.
|
||||
// 'verifying' and 'verified' statuses stay as is.
|
||||
// 'unverified' in the meaning of failed certificate generation stays as is.
|
||||
if ($rule->getAttribute('status') === 'created') {
|
||||
$rule->setAttribute('status', 'unverified');
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'rules' => $rules,
|
||||
'total' => $includeTotal ? $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT) : 0,
|
||||
'total' => $total ? $authorization->skip(fn () => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0,
|
||||
]), Response::MODEL_PROXY_RULE_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunction
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\Get as GetRule;
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule;
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule;
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\Verification\Update as UpdateRuleVerification;
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\Status\Update as UpdateRuleStatus;
|
||||
use Appwrite\Platform\Modules\Proxy\Http\Rules\XList as ListRules;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
@@ -26,6 +26,6 @@ class Http extends Service
|
||||
$this->addAction(GetRule::getName(), new GetRule());
|
||||
$this->addAction(ListRules::getName(), new ListRules());
|
||||
$this->addAction(DeleteRule::getName(), new DeleteRule());
|
||||
$this->addAction(UpdateRuleVerification::getName(), new UpdateRuleVerification());
|
||||
$this->addAction(UpdateRuleStatus::getName(), new UpdateRuleStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -36,6 +38,7 @@ class Create extends Base
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('resourceType', RESOURCE_TYPE_SITES)
|
||||
->label('event', 'variables.[variableId].create')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk', new Method(
|
||||
@@ -54,16 +57,18 @@ class Create extends Base
|
||||
]
|
||||
))
|
||||
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
|
||||
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project)
|
||||
public function action(string $siteId, string $variableId, string $key, string $value, bool $secret, Response $response, QueueEvent $queueForEvents, Database $dbForProject, Document $project)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
@@ -71,7 +76,7 @@ class Create extends Base
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variableId = ID::unique();
|
||||
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$variable = new Document([
|
||||
@@ -96,6 +101,8 @@ class Create extends Base
|
||||
'live' => false,
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('variableId', $variable->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> $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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,12 @@ class Specs extends Action
|
||||
'description' => 'Your secret dev API key',
|
||||
'in' => 'header',
|
||||
],
|
||||
'Cookie' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'Cookie',
|
||||
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Id',
|
||||
@@ -219,6 +225,18 @@ class Specs extends Action
|
||||
'description' => 'The user agent string of the client that made the request',
|
||||
'in' => 'header',
|
||||
],
|
||||
'DevKey' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Dev-Key',
|
||||
'description' => 'Your secret dev API key',
|
||||
'in' => 'header',
|
||||
],
|
||||
'Cookie' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'Cookie',
|
||||
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Id',
|
||||
@@ -272,7 +290,19 @@ class Specs extends Action
|
||||
'Cookie' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'Cookie',
|
||||
'description' => 'The user cookie to authenticate with',
|
||||
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'Session' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Session',
|
||||
'description' => 'The user session to authenticate with',
|
||||
'in' => 'header',
|
||||
],
|
||||
'DevKey' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Dev-Key',
|
||||
'description' => 'Your secret dev API key',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -338,6 +338,55 @@ class Migrations extends Action
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function getAPIKeyScopes(): array
|
||||
{
|
||||
return [
|
||||
'users.read',
|
||||
'users.write',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
'buckets.read',
|
||||
'buckets.write',
|
||||
'files.read',
|
||||
'files.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
'sites.read',
|
||||
'sites.write',
|
||||
'tokens.read',
|
||||
'tokens.write',
|
||||
'providers.read',
|
||||
'providers.write',
|
||||
'topics.read',
|
||||
'topics.write',
|
||||
'subscribers.read',
|
||||
'subscribers.write',
|
||||
'messages.read',
|
||||
'messages.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'oauth2.read',
|
||||
'oauth2.write',
|
||||
'mocks.read',
|
||||
'mocks.write',
|
||||
'project.policies.read',
|
||||
'project.policies.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -358,48 +407,7 @@ class Migrations extends Action
|
||||
METRIC_NETWORK_INBOUND,
|
||||
METRIC_NETWORK_OUTBOUND,
|
||||
],
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
'users.write',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
'buckets.read',
|
||||
'buckets.write',
|
||||
'files.read',
|
||||
'files.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
'sites.read',
|
||||
'sites.write',
|
||||
'tokens.read',
|
||||
'tokens.write',
|
||||
'providers.read',
|
||||
'providers.write',
|
||||
'topics.read',
|
||||
'topics.write',
|
||||
'subscribers.read',
|
||||
'subscribers.write',
|
||||
'messages.read',
|
||||
'messages.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'oauth2.read',
|
||||
'oauth2.write',
|
||||
'mocks.read',
|
||||
'mocks.write',
|
||||
'policies.read',
|
||||
'policies.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
]
|
||||
'scopes' => $this->getAPIKeyScopes(),
|
||||
]);
|
||||
|
||||
return API_KEY_EPHEMERAL . '_' . $apiKey;
|
||||
|
||||
@@ -437,6 +437,15 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
|
||||
break;
|
||||
case \Utopia\Database\Validator\BigInt::class:
|
||||
// BigInt validator reports Database::VAR_BIGINT, but OpenAPI expects scalar types.
|
||||
// We expose it as int64 to keep schema consistent with Column/Attribute models.
|
||||
$node['schema']['type'] = 'integer';
|
||||
$node['schema']['format'] = 'int64';
|
||||
if (!empty($param['example'])) {
|
||||
$node['schema']['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\Boolean::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: false;
|
||||
|
||||
@@ -23,6 +23,7 @@ class Attributes extends Validator
|
||||
protected array $supportedTypes = [
|
||||
Database::VAR_STRING,
|
||||
Database::VAR_INTEGER,
|
||||
Database::VAR_BIGINT,
|
||||
Database::VAR_FLOAT,
|
||||
Database::VAR_BOOLEAN,
|
||||
Database::VAR_DATETIME,
|
||||
@@ -181,9 +182,9 @@ class Attributes extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate signed only for integer/float types
|
||||
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
|
||||
// Validate signed only for integer/bigint/float types
|
||||
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer, bigint or float types";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -199,10 +200,10 @@ class Attributes extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate min/max range for integer/float
|
||||
// Validate min/max range for integer/bigint/float
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types";
|
||||
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer, bigint or float types";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ class Attributes extends Validator
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$min = $attribute['min'] ?? \PHP_INT_MIN;
|
||||
$max = $attribute['max'] ?? \PHP_INT_MAX;
|
||||
$rangeValidator = new Range($min, $max, Database::VAR_INTEGER);
|
||||
$rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
|
||||
if (!$rangeValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max";
|
||||
return false;
|
||||
@@ -272,6 +273,23 @@ class Attributes extends Validator
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_BIGINT:
|
||||
if (!is_int($attribute['default'])) {
|
||||
$this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be an integer";
|
||||
return false;
|
||||
}
|
||||
// Validate within range if min/max specified
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$min = $attribute['min'] ?? \PHP_INT_MIN;
|
||||
$max = $attribute['max'] ?? \PHP_INT_MAX;
|
||||
$rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
|
||||
if (!$rangeValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be between $min and $max";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_FLOAT:
|
||||
if (!is_float($attribute['default']) && !is_int($attribute['default'])) {
|
||||
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number";
|
||||
@@ -281,7 +299,7 @@ class Attributes extends Validator
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$min = $attribute['min'] ?? -\PHP_FLOAT_MAX;
|
||||
$max = $attribute['max'] ?? \PHP_FLOAT_MAX;
|
||||
$rangeValidator = new Range($min, $max, Database::VAR_FLOAT);
|
||||
$rangeValidator = new Range($min, $max, Range::TYPE_FLOAT);
|
||||
if (!$rangeValidator->isValid((float)$attribute['default'])) {
|
||||
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max";
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
|
||||
class V25 extends Filter
|
||||
{
|
||||
// Convert 1.9.3 params to 1.9.4
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'functions.createVariable':
|
||||
case 'sites.createVariable':
|
||||
$content = $this->fillVariableId($content);
|
||||
break;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillVariableId(array $content): array
|
||||
{
|
||||
$content['variableId'] = $content['variableId'] ?? 'unique()';
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_ATTRIBUTE_LIST = 'attributeList';
|
||||
public const MODEL_ATTRIBUTE_STRING = 'attributeString';
|
||||
public const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger';
|
||||
public const MODEL_ATTRIBUTE_BIGINT = 'attributeBigint';
|
||||
public const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat';
|
||||
public const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean';
|
||||
public const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail';
|
||||
@@ -98,6 +99,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_COLUMN_LIST = 'columnList';
|
||||
public const MODEL_COLUMN_STRING = 'columnString';
|
||||
public const MODEL_COLUMN_INTEGER = 'columnInteger';
|
||||
public const MODEL_COLUMN_BIGINT = 'columnBigint';
|
||||
public const MODEL_COLUMN_FLOAT = 'columnFloat';
|
||||
public const MODEL_COLUMN_BOOLEAN = 'columnBoolean';
|
||||
public const MODEL_COLUMN_EMAIL = 'columnEmail';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
class V25 extends Filter
|
||||
{
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
Response::MODEL_OAUTH2_OIDC => $this->parseOAuth2Oidc($content),
|
||||
Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => ($item['$id'] ?? null) === 'oidc' ? $this->parseOAuth2Oidc($item) : $item),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
|
||||
private function parseOAuth2Oidc(array $content): array
|
||||
{
|
||||
if (isset($content['tokenURL'])) {
|
||||
$content['tokenUrl'] = $content['tokenURL'];
|
||||
unset($content['tokenURL']);
|
||||
}
|
||||
|
||||
if (isset($content['userInfoURL'])) {
|
||||
$content['userInfoUrl'] = $content['userInfoURL'];
|
||||
unset($content['userInfoURL']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class AttributeBigInt extends Attribute
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Attribute Key.',
|
||||
'default' => '',
|
||||
'example' => 'count',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Attribute type.',
|
||||
'default' => '',
|
||||
'example' => 'bigint',
|
||||
])
|
||||
->addRule('min', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Minimum value to enforce for new documents.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 1,
|
||||
])
|
||||
->addRule('max', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Maximum value to enforce for new documents.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 10,
|
||||
])
|
||||
->addRule('default', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 10,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public array $conditions = [
|
||||
'type' => 'bigint'
|
||||
];
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'AttributeBigInt';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_ATTRIBUTE_BIGINT;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ class AttributeList extends Model
|
||||
->addRule('attributes', [
|
||||
'type' => [
|
||||
Response::MODEL_ATTRIBUTE_BOOLEAN,
|
||||
// BigInt must come before Integer: response model dispatch is "first match wins",
|
||||
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
|
||||
Response::MODEL_ATTRIBUTE_BIGINT,
|
||||
Response::MODEL_ATTRIBUTE_INTEGER,
|
||||
Response::MODEL_ATTRIBUTE_FLOAT,
|
||||
Response::MODEL_ATTRIBUTE_EMAIL,
|
||||
|
||||
@@ -62,6 +62,9 @@ class Collection extends Model
|
||||
->addRule('attributes', [
|
||||
'type' => [
|
||||
Response::MODEL_ATTRIBUTE_BOOLEAN,
|
||||
// BigInt must come before Integer: response model dispatch is "first match wins",
|
||||
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
|
||||
Response::MODEL_ATTRIBUTE_BIGINT,
|
||||
Response::MODEL_ATTRIBUTE_INTEGER,
|
||||
Response::MODEL_ATTRIBUTE_FLOAT,
|
||||
Response::MODEL_ATTRIBUTE_EMAIL,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class ColumnBigInt extends Column
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Column Key.',
|
||||
'default' => '',
|
||||
'example' => 'count',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Column type.',
|
||||
'default' => '',
|
||||
'example' => 'bigint',
|
||||
])
|
||||
->addRule('min', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Minimum value to enforce for new documents.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 1,
|
||||
])
|
||||
->addRule('max', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Maximum value to enforce for new documents.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 10,
|
||||
])
|
||||
->addRule('default', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'format' => 'int64',
|
||||
'description' => 'Default value for column when not provided. Cannot be set when column is required.',
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'example' => 10,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public array $conditions = [
|
||||
'type' => 'bigint'
|
||||
];
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'ColumnBigInt';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_COLUMN_BIGINT;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ class ColumnList extends Model
|
||||
->addRule('columns', [
|
||||
'type' => [
|
||||
Response::MODEL_COLUMN_BOOLEAN,
|
||||
// BigInt must come before Integer: response model dispatch is "first match wins",
|
||||
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
|
||||
Response::MODEL_COLUMN_BIGINT,
|
||||
Response::MODEL_COLUMN_INTEGER,
|
||||
Response::MODEL_COLUMN_FLOAT,
|
||||
Response::MODEL_COLUMN_EMAIL,
|
||||
|
||||
@@ -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' => '',
|
||||
|
||||
@@ -74,7 +74,7 @@ class Rule extends Model
|
||||
])
|
||||
->addRule('deploymentResourceId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID deployment\'s resource. Used if type is "deployment"',
|
||||
'description' => 'ID of deployment\'s resource (site or function ID). Used if type is "deployment"',
|
||||
'default' => '',
|
||||
'example' => 'n3u9feiwmf',
|
||||
])
|
||||
@@ -86,10 +86,10 @@ class Rule extends Model
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Domain verification status. Possible values are "created", "verifying", "verified" and "unverified"',
|
||||
'default' => 'created',
|
||||
'description' => 'Domain verification status. Possible values are "unverified", "verifying", "verified"',
|
||||
'default' => 'unverified',
|
||||
'example' => 'verified',
|
||||
'enum' => ['created', 'verifying', 'verified', 'unverified'],
|
||||
'enum' => ['unverified', 'verifying', 'verified'],
|
||||
])
|
||||
->addRule('logs', [
|
||||
'type' => self::TYPE_STRING,
|
||||
|
||||
@@ -63,6 +63,9 @@ class Table extends Model
|
||||
->addRule('columns', [
|
||||
'type' => [
|
||||
Response::MODEL_COLUMN_BOOLEAN,
|
||||
// BigInt must come before Integer: response model dispatch is "first match wins",
|
||||
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
|
||||
Response::MODEL_COLUMN_BIGINT,
|
||||
Response::MODEL_COLUMN_INTEGER,
|
||||
Response::MODEL_COLUMN_FLOAT,
|
||||
Response::MODEL_COLUMN_EMAIL,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Executor;
|
||||
|
||||
class Exception extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Executor\Exception;
|
||||
|
||||
use Executor\Exception;
|
||||
use Throwable;
|
||||
|
||||
class Timeout extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
private readonly int $timeoutSeconds,
|
||||
int $code = 0,
|
||||
?Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getTimeoutSeconds(): int
|
||||
{
|
||||
return $this->timeoutSeconds;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -4163,4 +4163,72 @@ class AccountCustomClientTest extends Scope
|
||||
|
||||
$this->assertEquals(401, $verification3['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testRefreshEmailPasswordSession(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
|
||||
$account = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $account['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
$this->assertNotEmpty($session['body']['$id']);
|
||||
|
||||
$sessionId = $session['body']['$id'];
|
||||
$cookie = 'a_session_' . $this->getProject()['$id'] . '=' .$session['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $session['headers']['status-code']);
|
||||
$this->assertNotEmpty($session['body']['expire']);
|
||||
$expiryBefore = $session['body']['expire'];
|
||||
|
||||
\sleep(3); // Small delay to ensure expiry an expand
|
||||
|
||||
$session = $this->client->call(Client::METHOD_PATCH, '/account/sessions/' . $sessionId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $session['headers']['status-code']);
|
||||
$this->assertNotEmpty($session['body']['expire']);
|
||||
$expiryAfter = $session['body']['expire'];
|
||||
|
||||
$this->assertGreaterThan(\strtotime($expiryBefore), \strtotime($expiryAfter));
|
||||
|
||||
$session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $session['headers']['status-code']);
|
||||
$this->assertEquals(\strtotime($expiryAfter), \strtotime($session['body']['expire']));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class FunctionsConsoleClientTest extends Scope
|
||||
{
|
||||
@@ -70,6 +72,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST',
|
||||
'value' => 'TESTINGVALUE',
|
||||
'secret' => false
|
||||
@@ -82,6 +85,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$secretVariable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST_1',
|
||||
'value' => 'TESTINGVALUE_1',
|
||||
'secret' => true
|
||||
@@ -196,6 +200,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST',
|
||||
'value' => 'TESTINGVALUE',
|
||||
'secret' => false
|
||||
@@ -208,6 +213,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST_1',
|
||||
'value' => 'TESTINGVALUE_1',
|
||||
'secret' => true
|
||||
@@ -226,6 +232,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST',
|
||||
'value' => 'ANOTHERTESTINGVALUE',
|
||||
'secret' => false
|
||||
@@ -234,10 +241,47 @@ class FunctionsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(409, $variable['headers']['status-code']);
|
||||
|
||||
// Test for invalid variableId
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => '!invalid-id!',
|
||||
'key' => 'INVALID_ID_KEY',
|
||||
'value' => 'value',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $variable['headers']['status-code']);
|
||||
|
||||
// Test for duplicate variableId
|
||||
$duplicateVariableId = ID::unique();
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => $duplicateVariableId,
|
||||
'key' => 'DUP_ID_KEY_1',
|
||||
'value' => 'value1',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $variable['headers']['status-code']);
|
||||
|
||||
$duplicate = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => $duplicateVariableId,
|
||||
'key' => 'DUP_ID_KEY_2',
|
||||
'value' => 'value2',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(409, $duplicate['headers']['status-code']);
|
||||
|
||||
// Test for invalid key
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => str_repeat("A", 256),
|
||||
'value' => 'TESTINGVALUE'
|
||||
]
|
||||
@@ -249,6 +293,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variable = $this->createVariable(
|
||||
$functionId,
|
||||
[
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'LONGKEY',
|
||||
'value' => str_repeat("#", 8193),
|
||||
]
|
||||
@@ -283,6 +328,150 @@ class FunctionsConsoleClientTest extends Scope
|
||||
*/
|
||||
}
|
||||
|
||||
public function testListVariablesWithLimit(): void
|
||||
{
|
||||
// Create a fresh function for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test List Variables With Limit',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable1 = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'LIMIT_KEY_1',
|
||||
'value' => 'limit-value-1',
|
||||
]);
|
||||
$this->assertEquals(201, $variable1['headers']['status-code']);
|
||||
|
||||
$variable2 = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'LIMIT_KEY_2',
|
||||
'value' => 'limit-value-2',
|
||||
]);
|
||||
$this->assertEquals(201, $variable2['headers']['status-code']);
|
||||
|
||||
// List with limit of 1
|
||||
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
],
|
||||
'total' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['variables']);
|
||||
$this->assertGreaterThanOrEqual(2, $response['body']['total']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testListVariablesWithoutTotal(): void
|
||||
{
|
||||
// Create a fresh function for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test List Variables Without Total',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'NO_TOTAL_KEY',
|
||||
'value' => 'no-total-value',
|
||||
]);
|
||||
$this->assertEquals(201, $variable['headers']['status-code']);
|
||||
|
||||
// List with total=false
|
||||
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'total' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, \count($response['body']['variables']));
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testListVariablesCursorPagination(): void
|
||||
{
|
||||
// Create a fresh function for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test List Variables Cursor Pagination',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable1 = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'CURSOR_KEY_1',
|
||||
'value' => 'cursor-value-1',
|
||||
]);
|
||||
$this->assertEquals(201, $variable1['headers']['status-code']);
|
||||
|
||||
$variable2 = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'CURSOR_KEY_2',
|
||||
'value' => 'cursor-value-2',
|
||||
]);
|
||||
$this->assertEquals(201, $variable2['headers']['status-code']);
|
||||
|
||||
// Get first page with limit 1
|
||||
$page1 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
],
|
||||
'total' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $page1['headers']['status-code']);
|
||||
$this->assertCount(1, $page1['body']['variables']);
|
||||
$cursorId = $page1['body']['variables'][0]['$id'];
|
||||
|
||||
// Get next page using cursor
|
||||
$page2 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
|
||||
],
|
||||
'total' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $page2['headers']['status-code']);
|
||||
$this->assertCount(1, $page2['body']['variables']);
|
||||
$this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testGetVariable(): void
|
||||
{
|
||||
$data = $this->setupTestVariables();
|
||||
@@ -337,6 +526,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST',
|
||||
'value' => 'TESTINGVALUE',
|
||||
'secret' => false
|
||||
@@ -345,6 +535,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variableId = $variable['body']['$id'];
|
||||
|
||||
$secretVariable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST_1',
|
||||
'value' => 'TESTINGVALUE_1',
|
||||
'secret' => true
|
||||
@@ -457,6 +648,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
||||
// Update with no parameters should fail with 400
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -464,6 +656,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
// Update with only value should succeed
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -471,7 +664,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
'value' => 'TESTINGVALUEUPDATED_2'
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$longKey = str_repeat("A", 256);
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
|
||||
@@ -496,6 +689,110 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateVariableKey(): void
|
||||
{
|
||||
// Create a fresh function and variable for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test Update Variable Key',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'KEY_BEFORE',
|
||||
'value' => 'unchanged-value',
|
||||
'secret' => false
|
||||
]);
|
||||
$this->assertEquals(201, $variable['headers']['status-code']);
|
||||
$variableId = $variable['body']['$id'];
|
||||
|
||||
// Update only key (key is nullable, but we provide a new key)
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'KEY_AFTER',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('KEY_AFTER', $response['body']['key']);
|
||||
$this->assertEquals('unchanged-value', $response['body']['value']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testUpdateVariableValueOnly(): void
|
||||
{
|
||||
// Create a fresh function and variable for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test Update Variable Value',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'UNCHANGED_KEY',
|
||||
'value' => 'value-before',
|
||||
'secret' => false
|
||||
]);
|
||||
$this->assertEquals(201, $variable['headers']['status-code']);
|
||||
$variableId = $variable['body']['$id'];
|
||||
|
||||
// Update only value
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'value' => 'value-after',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('UNCHANGED_KEY', $response['body']['key']);
|
||||
$this->assertEquals('value-after', $response['body']['value']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testUpdateVariableNotFound(): void
|
||||
{
|
||||
// Create a fresh function for this test
|
||||
$function = $this->createFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test Update Variable Not Found',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/non-existent-id', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'NEW_KEY',
|
||||
'value' => 'new-value',
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
$this->assertEquals('variable_not_found', $response['body']['type']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testDeleteVariable(): void
|
||||
{
|
||||
// Create a fresh function and variables for this test since it deletes them
|
||||
@@ -512,6 +809,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST',
|
||||
'value' => 'TESTINGVALUE',
|
||||
'secret' => false
|
||||
@@ -520,6 +818,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
$variableId = $variable['body']['$id'];
|
||||
|
||||
$secretVariable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'APP_TEST_1',
|
||||
'value' => 'TESTINGVALUE_1',
|
||||
'secret' => true
|
||||
@@ -585,6 +884,7 @@ class FunctionsConsoleClientTest extends Scope
|
||||
|
||||
// create variable
|
||||
$variable = $this->createVariable($functionId, [
|
||||
'variableId' => ID::unique(),
|
||||
'key' => 'CUSTOM_VARIABLE',
|
||||
'value' => 'a_secret_value',
|
||||
'secret' => true,
|
||||
|
||||
@@ -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'
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1096,6 +1096,7 @@ trait MigrationsBase
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-response-format' => '1.9.3'
|
||||
], [
|
||||
'key' => 'TEST_VAR',
|
||||
'value' => 'test_value',
|
||||
@@ -1256,7 +1257,6 @@ trait MigrationsBase
|
||||
'max' => 65,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $response['headers']['status-code']);
|
||||
$this->assertEquals($response['body']['key'], 'age');
|
||||
$this->assertEquals($response['body']['type'], 'integer');
|
||||
@@ -1573,6 +1573,19 @@ trait MigrationsBase
|
||||
|
||||
$this->assertEquals(202, $varchar['headers']['status-code']);
|
||||
|
||||
$bigint = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/bigint', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'bigint',
|
||||
'min' => 2147483648,
|
||||
'max' => 9223372036854775807,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $bigint['headers']['status-code']);
|
||||
|
||||
$mediumtext = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/mediumtext', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -1623,6 +1636,7 @@ trait MigrationsBase
|
||||
'mediumtext' => 'mediumText',
|
||||
'longtext' => 'longText',
|
||||
'varchar' => 'varchar',
|
||||
'bigint' => 2147483648 + $i,
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -1711,6 +1725,8 @@ trait MigrationsBase
|
||||
$this->assertStringContainsString('mediumText', $csvData, 'CSV should contain the medium column header');
|
||||
$this->assertStringContainsString('longText', $csvData, 'CSV should contain the long text column header');
|
||||
$this->assertStringContainsString('varchar', $csvData, 'CSV should contain the varchar column header');
|
||||
$this->assertStringContainsString('bigint', $csvData, 'CSV should contain the bigint column header');
|
||||
$this->assertStringContainsString('2147483649', $csvData, 'CSV should contain bigint test data');
|
||||
|
||||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Project;
|
||||
use PHPUnit\Framework\Attributes\Before;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
trait OAuth2Base
|
||||
{
|
||||
@@ -172,6 +173,40 @@ trait OAuth2Base
|
||||
$this->assertNotContains('mock-unverified', $ids);
|
||||
}
|
||||
|
||||
public function testListOAuth2ProvidersTotalFalse(): void
|
||||
{
|
||||
$response = $this->listOAuth2Providers(total: false);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(0, $response['body']['total']);
|
||||
$this->assertGreaterThan(0, \count($response['body']['providers']));
|
||||
}
|
||||
|
||||
public function testListOAuth2ProvidersWithLimit(): void
|
||||
{
|
||||
$response = $this->listOAuth2Providers([
|
||||
Query::limit(1)->toString(),
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['providers']);
|
||||
$this->assertGreaterThan(1, $response['body']['total']);
|
||||
}
|
||||
|
||||
public function testListOAuth2ProvidersWithOffset(): void
|
||||
{
|
||||
$listAll = $this->listOAuth2Providers();
|
||||
$this->assertSame(200, $listAll['headers']['status-code']);
|
||||
|
||||
$listOffset = $this->listOAuth2Providers([
|
||||
Query::offset(1)->toString(),
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $listOffset['headers']['status-code']);
|
||||
$this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']);
|
||||
$this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Get OAuth2 provider
|
||||
// =========================================================================
|
||||
@@ -188,6 +223,28 @@ trait OAuth2Base
|
||||
$this->assertSame('', $response['body']['clientSecret']);
|
||||
}
|
||||
|
||||
public function testGetOAuth2ProviderWithAlias(): void
|
||||
{
|
||||
// The action declares the canonical param name as `providerId` and
|
||||
// registers `provider` as an alias so that older SDK versions that
|
||||
// send the provider in the query string continue to work.
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
|
||||
// Call with `provider` in query string (legacy behaviour)
|
||||
$response = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/project/oauth2/github?provider=github',
|
||||
$headers,
|
||||
);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('github', $response['body']['$id']);
|
||||
}
|
||||
|
||||
public function testGetOAuth2ProviderClientSecretWriteOnly(): void
|
||||
{
|
||||
$this->updateOAuth2('amazon', [
|
||||
@@ -221,19 +278,23 @@ trait OAuth2Base
|
||||
|
||||
public function testGetOAuth2ProviderUnsupported(): void
|
||||
{
|
||||
// The `providerId` param is validated by a WhiteList of registered
|
||||
// OAuth2 provider keys, so an unknown value is rejected at validation
|
||||
// time — before the action runs — and surfaces as a generic argument
|
||||
// error rather than `project_provider_unsupported`.
|
||||
$response = $this->getOAuth2Provider('not-a-real-provider');
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('project_provider_unsupported', $response['body']['type']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void
|
||||
{
|
||||
// `mock` is present in oAuthProviders config (enabled: true) but is NOT
|
||||
// registered in Base::getProviderActions(). Get::action has two
|
||||
// separate `unsupported` throw branches — testGetOAuth2ProviderUnsupported
|
||||
// covers the first (provider missing from config); this covers the
|
||||
// second (provider in config but missing from the action registry).
|
||||
// `mock` is present in oAuthProviders config (enabled: true) but is
|
||||
// NOT registered in Base::getProviderActions(). It passes the
|
||||
// WhiteList validator (which only checks config membership) and
|
||||
// reaches the action body, where the action-registry check throws
|
||||
// `project_provider_unsupported`.
|
||||
$response = $this->getOAuth2Provider('mock');
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1590,8 +1651,8 @@ trait OAuth2Base
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']);
|
||||
$this->assertArrayHasKey('authorizationURL', $response['body']);
|
||||
$this->assertArrayHasKey('tokenUrl', $response['body']);
|
||||
$this->assertArrayHasKey('userInfoUrl', $response['body']);
|
||||
$this->assertArrayHasKey('tokenURL', $response['body']);
|
||||
$this->assertArrayHasKey('userInfoURL', $response['body']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
@@ -1599,8 +1660,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1611,15 +1672,15 @@ trait OAuth2Base
|
||||
'clientId' => 'oidc-discovery',
|
||||
'clientSecret' => 'oidc-discovery-secret',
|
||||
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
@@ -1627,8 +1688,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1640,8 +1701,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
@@ -1670,8 +1731,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
@@ -1679,7 +1740,7 @@ trait OAuth2Base
|
||||
'clientId' => 'oidc-partial',
|
||||
'clientSecret' => 'oidc-partial-secret',
|
||||
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
@@ -1692,8 +1753,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1724,8 +1785,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1755,8 +1816,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1770,8 +1831,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
@@ -1780,7 +1841,7 @@ trait OAuth2Base
|
||||
'clientId' => 'oidc-split-discovery',
|
||||
'clientSecret' => 'oidc-split-discovery-secret',
|
||||
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
@@ -1788,19 +1849,19 @@ trait OAuth2Base
|
||||
// state must include the two stored URLs + the new one to satisfy
|
||||
// the all-three-discovery-URLs branch of the enable check.
|
||||
$enable = $this->updateOAuth2('oidc', [
|
||||
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Confirm all three URLs ended up persisted (merge wrote the new
|
||||
// userInfoUrl while preserving the previously stored two).
|
||||
// userInfoURL while preserving the previously stored two).
|
||||
$get = $this->getOAuth2Provider('oidc');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
@@ -1808,8 +1869,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1822,8 +1883,8 @@ trait OAuth2Base
|
||||
'clientSecret' => 'oidc-clear-then-enable-secret',
|
||||
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
@@ -1846,8 +1907,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1868,16 +1929,16 @@ trait OAuth2Base
|
||||
$switch = $this->updateOAuth2('oidc', [
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $switch['headers']['status-code']);
|
||||
$this->assertTrue($switch['body']['enabled']);
|
||||
$this->assertSame('', $switch['body']['wellKnownURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
@@ -1885,8 +1946,8 @@ trait OAuth2Base
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1900,23 +1961,23 @@ trait OAuth2Base
|
||||
'clientSecret' => 'oidc-clear-secret',
|
||||
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
|
||||
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'tokenURL' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$response = $this->updateOAuth2('oidc', [
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenUrl' => '',
|
||||
'userInfoUrl' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('', $response['body']['wellKnownURL']);
|
||||
$this->assertSame('', $response['body']['authorizationURL']);
|
||||
$this->assertSame('', $response['body']['tokenUrl']);
|
||||
$this->assertSame('', $response['body']['userInfoUrl']);
|
||||
$this->assertSame('', $response['body']['tokenURL']);
|
||||
$this->assertSame('', $response['body']['userInfoURL']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
@@ -1926,6 +1987,96 @@ trait OAuth2Base
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void
|
||||
{
|
||||
// Reset to clean state
|
||||
$this->updateOAuth2('oidc', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'wellKnownURL' => '',
|
||||
'authorizationURL' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.3',
|
||||
];
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
|
||||
// Update using OLD param names (aliases must still work)
|
||||
$response = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/project/oauth2/oidc',
|
||||
$headers,
|
||||
[
|
||||
'clientId' => 'oidc-compat-client',
|
||||
'clientSecret' => 'oidc-compat-secret',
|
||||
'tokenUrl' => 'https://idp.example.com/oauth2/token',
|
||||
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
|
||||
'enabled' => false,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertArrayHasKey('tokenUrl', $response['body']);
|
||||
$this->assertArrayHasKey('userInfoUrl', $response['body']);
|
||||
$this->assertArrayNotHasKey('tokenURL', $response['body']);
|
||||
$this->assertArrayNotHasKey('userInfoURL', $response['body']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
|
||||
|
||||
// GET with 1.9.3 format must also return old param names
|
||||
$get = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/project/oauth2/oidc',
|
||||
$headers,
|
||||
);
|
||||
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertArrayHasKey('tokenUrl', $get['body']);
|
||||
$this->assertArrayHasKey('userInfoUrl', $get['body']);
|
||||
$this->assertArrayNotHasKey('tokenURL', $get['body']);
|
||||
$this->assertArrayNotHasKey('userInfoURL', $get['body']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
|
||||
|
||||
// LIST with 1.9.3 format must also return old param names for OIDC
|
||||
$list = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/project/oauth2',
|
||||
$headers,
|
||||
);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$oidcEntry = null;
|
||||
foreach ($list['body']['providers'] as $provider) {
|
||||
if ($provider['$id'] === 'oidc') {
|
||||
$oidcEntry = $provider;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response');
|
||||
$this->assertArrayHasKey('tokenUrl', $oidcEntry);
|
||||
$this->assertArrayHasKey('userInfoUrl', $oidcEntry);
|
||||
$this->assertArrayNotHasKey('tokenURL', $oidcEntry);
|
||||
$this->assertArrayNotHasKey('userInfoURL', $oidcEntry);
|
||||
$this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']);
|
||||
$this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('oidc', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'tokenURL' => '',
|
||||
'userInfoURL' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Okta (clientId + clientSecret + optional domain/authServer)
|
||||
// =========================================================================
|
||||
@@ -2573,7 +2724,7 @@ trait OAuth2Base
|
||||
);
|
||||
}
|
||||
|
||||
protected function getOAuth2Provider(string $provider, bool $authenticated = true): mixed
|
||||
protected function getOAuth2Provider(string $providerId, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
@@ -2586,13 +2737,23 @@ trait OAuth2Base
|
||||
|
||||
return $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/project/oauth2/' . $provider,
|
||||
'/project/oauth2/' . $providerId,
|
||||
$headers,
|
||||
);
|
||||
}
|
||||
|
||||
protected function listOAuth2Providers(bool $authenticated = true): mixed
|
||||
protected function listOAuth2Providers(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [];
|
||||
|
||||
if ($queries !== null) {
|
||||
$params['queries'] = $queries;
|
||||
}
|
||||
|
||||
if ($total !== null) {
|
||||
$params['total'] = $total;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -2606,6 +2767,7 @@ trait OAuth2Base
|
||||
Client::METHOD_GET,
|
||||
'/project/oauth2',
|
||||
$headers,
|
||||
$params,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ class WebhooksCustomServerTest extends Scope
|
||||
$this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.3',
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'key1',
|
||||
'value' => 'value1',
|
||||
@@ -699,6 +700,7 @@ class WebhooksCustomServerTest extends Scope
|
||||
$variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.3',
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'key1',
|
||||
'value' => 'value1',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,298 +2,753 @@
|
||||
|
||||
namespace Tests\E2E\Services\Proxy;
|
||||
|
||||
use Appwrite\ID;
|
||||
use Appwrite\Tests\Async;
|
||||
use CURLFile;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\System\System;
|
||||
|
||||
trait ProxyBase
|
||||
{
|
||||
use Async;
|
||||
use ProxyHelpers;
|
||||
|
||||
protected function listRules(array $params = []): mixed
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createAPIRule(string $domain): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
// Cleanup for testRuleVerification test
|
||||
// Required as it uses static domain name
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::endsWith('domain', 'webapp.com')->toString(),
|
||||
Query::limit(1000)->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$ruleId = $rule['$id'];
|
||||
$response = $this->deleteRule($ruleId);
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
return $rule;
|
||||
if ($rules['body']['total'] > 0) {
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::endsWith('domain', 'webapp.com')->toString(),
|
||||
Query::limit(1)->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, count($rules['body']['rules']));
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateRuleVerification(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'siteId' => $siteId,
|
||||
'branch' => $branch,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function getRule(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'url' => $url,
|
||||
'statusCode' => $statusCode,
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'functionId' => $functionId,
|
||||
'branch' => $branch,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function deleteRule(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function setupAPIRule(string $domain): string
|
||||
public function testCreateRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-api.myapp.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals($domain, $rule['body']['domain']);
|
||||
$this->assertEquals('manual', $rule['body']['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rule['body']);
|
||||
$this->assertArrayHasKey('domain', $rule['body']);
|
||||
$this->assertArrayHasKey('type', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectUrl', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
|
||||
$this->assertArrayHasKey('logs', $rule['body']);
|
||||
$this->assertArrayHasKey('renewAt', $rule['body']);
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
|
||||
{
|
||||
$rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(409, $rule['headers']['status-code']);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
|
||||
{
|
||||
$rule = $this->createFunctionRule($domain, $functionId, $branch);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
|
||||
{
|
||||
$rule = $this->createSiteRule($domain, $siteId, $branch);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function cleanupRule(string $ruleId): void
|
||||
{
|
||||
$rule = $this->deleteRule($ruleId);
|
||||
$this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
|
||||
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
protected function cleanupSite(string $siteId): void
|
||||
public function testCreateRuleSetup(): void
|
||||
{
|
||||
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
|
||||
$ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
protected function cleanupFunction(string $functionId): void
|
||||
public function testCreateRuleApex(): void
|
||||
{
|
||||
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
|
||||
$domain = \uniqid() . '.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
}
|
||||
|
||||
protected function setupSite(): mixed
|
||||
public function testCreateRuleVcs(): void
|
||||
{
|
||||
// Site
|
||||
$site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'siteId' => ID::unique(),
|
||||
'name' => 'Proxy site',
|
||||
'framework' => 'other',
|
||||
'adapter' => 'static',
|
||||
'buildRuntime' => 'static-1',
|
||||
'outputDirectory' => './',
|
||||
'buildCommand' => '',
|
||||
'installCommand' => '',
|
||||
'fallbackFile' => '',
|
||||
]);
|
||||
$domain = \uniqid() . '-vcs.myapp.com';
|
||||
|
||||
$this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$siteId = $site['body']['$id'];
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
// Deployment
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'code' => $this->packageSite('static'),
|
||||
'activate' => 'true'
|
||||
]);
|
||||
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
|
||||
$deploymentId = $deployment['body']['$id'] ?? '';
|
||||
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$this->assertEventually(function () use ($siteId, $deploymentId) {
|
||||
$site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
$this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
|
||||
}, 120000, 500);
|
||||
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
|
||||
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
|
||||
$domain = \uniqid() . '-vcs.' . $sitesDomain;
|
||||
|
||||
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
}
|
||||
|
||||
protected function setupFunction(): mixed
|
||||
public function testCreateAPIRule(): void
|
||||
{
|
||||
// Function
|
||||
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'functionId' => ID::unique(),
|
||||
'runtime' => 'node-22',
|
||||
'name' => 'Proxy Function',
|
||||
'entrypoint' => 'index.js',
|
||||
'commands' => '',
|
||||
'execute' => ['any']
|
||||
$domain = \uniqid() . '-api.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$ruleId = $this->setupAPIRule($domain);
|
||||
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$rule = $this->createAPIRule('http://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule('https://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule('wss://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule($domain . '/some-path');
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateRedirectRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-redirect.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$siteId = $this->setupSite()['siteId'];
|
||||
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['id']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['id']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$domain = \uniqid() . '-redirect-307.custom.localhost';
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
|
||||
$this->assertEquals(307, $response['headers']['status-code']);
|
||||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('type', ['redirect'])->toString(),
|
||||
Query::equal('trigger', ['manual'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
|
||||
$this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
$this->cleanupSite($siteId);
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
$functionId = $function['body']['$id'];
|
||||
public function testCreateFunctionRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-function.custom.localhost';
|
||||
|
||||
// Deployment
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'code' => $this->packageFunction('basic'),
|
||||
'activate' => 'true'
|
||||
]);
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
|
||||
$deploymentId = $deployment['body']['$id'] ?? '';
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$setup = $this->setupFunction();
|
||||
$functionId = $setup['functionId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($functionId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupFunctionRule($domain, $functionId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
|
||||
$this->assertEventually(function () use ($functionId, $deploymentId) {
|
||||
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
$this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
}, 100000, 500);
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['function'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$functionId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentId', [$deploymentId])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
});
|
||||
}
|
||||
|
||||
private function packageSite(string $site): CURLFile
|
||||
public function testCreateSiteRule(): void
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$domain = \uniqid() . '-site.custom.localhost';
|
||||
|
||||
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
|
||||
$tarPath = "$folderPath/code.tar.gz";
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
if (filesize($tarPath) > 1024 * 1024 * 5) {
|
||||
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupSiteRule($domain, $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertSame(200, $rule['headers']['status-code']);
|
||||
$this->assertSame('unverified', $rule['body']['status']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('Contact page', $response['body']);
|
||||
|
||||
// Wildcard domains automatically get verified status
|
||||
$domains = [
|
||||
\uniqid() . '.sites.localhost',
|
||||
\uniqid() . '.rebranded.localhost',
|
||||
];
|
||||
foreach ($domains as $domain) {
|
||||
$wildcardRuleId = $this->setupSiteRule($domain, $siteId);
|
||||
$this->assertNotEmpty($wildcardRuleId);
|
||||
$rule = $this->getRule($wildcardRuleId);
|
||||
$this->assertSame(200, $rule['headers']['status-code']);
|
||||
$this->assertSame('verified', $rule['body']['status']);
|
||||
$this->cleanupRule($wildcardRuleId);
|
||||
}
|
||||
|
||||
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('trigger', ['deployment'])->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertGreaterThan(0, $rules['body']['total']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupSite($siteId);
|
||||
|
||||
$this->assertEventually(function () use ($siteId, $deploymentId) {
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentId', [$deploymentId])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
});
|
||||
}
|
||||
|
||||
private function packageFunction(string $function): CURLFile
|
||||
public function testCreateSiteBranchRule(): void
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$domain = \uniqid() . '-site-branch.custom.localhost';
|
||||
|
||||
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
|
||||
$tarPath = "$folderPath/code.tar.gz";
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
if (filesize($tarPath) > 1024 * 1024 * 5) {
|
||||
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
|
||||
$ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testCreateFunctionBranchRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-function-branch.custom.localhost';
|
||||
|
||||
$setup = $this->setupFunction();
|
||||
$functionId = $setup['functionId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($functionId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testUpdateRule(): void
|
||||
{
|
||||
// Create function appwrite-network domain
|
||||
$functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0];
|
||||
$domain = \uniqid() . '-cname-api.' . $functionsDomain;
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// Create site appwrite-network domain
|
||||
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
|
||||
$domain = \uniqid() . '-cname-api.' . $sitesDomain;
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// Create + update
|
||||
$domain = \uniqid() . '-cname-api.custom.com';
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testGetRule()
|
||||
{
|
||||
$domain = \uniqid() . '-get.custom.localhost';
|
||||
$ruleId = $this->setupAPIRule($domain);
|
||||
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals($domain, $rule['body']['domain']);
|
||||
$this->assertEquals('manual', $rule['body']['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rule['body']);
|
||||
$this->assertArrayHasKey('domain', $rule['body']);
|
||||
$this->assertArrayHasKey('type', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectUrl', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
|
||||
$this->assertArrayHasKey('logs', $rule['body']);
|
||||
$this->assertArrayHasKey('renewAt', $rule['body']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testListRules()
|
||||
{
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$rule = $this->deleteRule($rule['$id']);
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
$rule1Domain = \uniqid() . '-list1.custom.localhost';
|
||||
$rule1Id = $this->setupAPIRule($rule1Domain);
|
||||
$this->assertNotEmpty($rule1Id);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(1, $rules['body']['total']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
|
||||
|
||||
$this->assertEquals('manual', $rules['body']['rules'][0]['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('domain', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('type', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
|
||||
|
||||
$rule2Domain = \uniqid() . '-list1.custom.localhost';
|
||||
$rule2Id = $this->setupAPIRule($rule2Domain);
|
||||
$this->assertNotEmpty($rule2Id);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
$this->assertCount(2, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('$id', [$rule1Id])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::orderDesc('$id')->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(2, $rules['body']['rules']);
|
||||
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('domain', [$rule2Domain])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule1Domain,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleIds = \array_column($rules['body']['rules'], '$id');
|
||||
$this->assertContains($rule1Id, $ruleIds);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule2Domain,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleIds = \array_column($rules['body']['rules'], '$id');
|
||||
$this->assertContains($rule2Id, $ruleIds);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule1Id,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
|
||||
$this->assertContains($rule1Domain, $ruleDomains);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule2Id,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
|
||||
$this->assertContains($rule2Domain, $ruleDomains);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$rule = $this->deleteRule($rule['$id']);
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
}
|
||||
|
||||
public function testRuleVerification(): void
|
||||
{
|
||||
|
||||
// 1. Site rule can verify
|
||||
$site = $this->setupSite();
|
||||
$siteId = $site['siteId'];
|
||||
|
||||
$rule = $this->createSiteRule('stage-site.webapp.com', $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
$this->assertNotEmpty($rule['body']['$id']);
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals($ruleId, $rule['body']['$id']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
$this->cleanupSite($siteId);
|
||||
|
||||
// 2. Function rule can verify
|
||||
$function = $this->setupFunction();
|
||||
$functionId = $function['functionId'];
|
||||
|
||||
$rule = $this->createFunctionRule('stage-function.webapp.com', $functionId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$rule = $this->createAPIRule('stage-site.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
|
||||
// 3. Wrong A record fails to verify
|
||||
$rule = $this->createAPIRule('wrong-a-webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 4. Correct A record can verify
|
||||
$rule = $this->createAPIRule('webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// 5. Correct CNAME record can verify (no CAA record)
|
||||
$rule = $this->createAPIRule('stage.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// 6. Missing CNAME record fails to verify
|
||||
$rule = $this->createAPIRule('stage-missing-cname.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 7. Wrong CNAME record fails to verify
|
||||
$rule = $this->createAPIRule('stage-wrong-cname.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 8. Wrong CAA record fails to verify
|
||||
$rule = $this->createAPIRule('stage-wrong-caa.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleStatus($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 9. Correct CAA record can verify
|
||||
$rule = $this->createAPIRule('stage-correct-caa.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
}
|
||||
|
||||
public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void
|
||||
{
|
||||
$domain = \uniqid() . '-timestamp-test.webapp.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $rule['body']['status']);
|
||||
$this->assertNotEmpty($rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$initialUpdatedAt = $rule['body']['$updatedAt'];
|
||||
$initiallogs = $rule['body']['logs'];
|
||||
|
||||
sleep(1);
|
||||
|
||||
$updatedRule = $this->updateRuleStatus($ruleId);
|
||||
|
||||
$this->assertEquals(400, $updatedRule['headers']['status-code']);
|
||||
$this->assertStringContainsString($initiallogs, $updatedRule['body']['message']);
|
||||
|
||||
$ruleAfterUpdate = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']);
|
||||
$this->assertEquals('unverified', $ruleAfterUpdate['body']['status']);
|
||||
$this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']);
|
||||
$this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']);
|
||||
|
||||
$initialTime = new \DateTime($initialUpdatedAt);
|
||||
$updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']);
|
||||
$this->assertGreaterThan($initialTime, $updatedTime);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Proxy;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class ProxyConsoleClientTest extends Scope
|
||||
{
|
||||
use ProxyBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -2,758 +2,13 @@
|
||||
|
||||
namespace Tests\E2E\Services\Proxy;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\System\System;
|
||||
|
||||
class ProxyCustomServerTest extends Scope
|
||||
{
|
||||
use ProxyBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Cleanup for testRuleVerification test
|
||||
// Required as it uses static domain name
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::endsWith('domain', 'webapp.com')->toString(),
|
||||
Query::limit(1000)->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$ruleId = $rule['$id'];
|
||||
$response = $this->deleteRule($ruleId);
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
if ($rules['body']['total'] > 0) {
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::endsWith('domain', 'webapp.com')->toString(),
|
||||
Query::limit(1)->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, count($rules['body']['rules']));
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testCreateRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-api.myapp.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals($domain, $rule['body']['domain']);
|
||||
$this->assertEquals('manual', $rule['body']['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rule['body']);
|
||||
$this->assertArrayHasKey('domain', $rule['body']);
|
||||
$this->assertArrayHasKey('type', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectUrl', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
|
||||
$this->assertArrayHasKey('logs', $rule['body']);
|
||||
$this->assertArrayHasKey('renewAt', $rule['body']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(409, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->deleteRule($ruleId);
|
||||
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateRuleSetup(): void
|
||||
{
|
||||
$ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testCreateRuleApex(): void
|
||||
{
|
||||
$domain = \uniqid() . '.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
}
|
||||
|
||||
public function testCreateRuleVcs(): void
|
||||
{
|
||||
$domain = \uniqid() . '-vcs.myapp.com';
|
||||
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
|
||||
$domain = \uniqid() . '-vcs.' . $sitesDomain;
|
||||
|
||||
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateAPIRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-api.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$ruleId = $this->setupAPIRule($domain);
|
||||
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$rule = $this->createAPIRule('http://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule('https://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule('wss://' . $domain);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$rule = $this->createAPIRule($domain . '/some-path');
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateRedirectRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-redirect.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$siteId = $this->setupSite()['siteId'];
|
||||
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['id']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['id']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$domain = \uniqid() . '-redirect-307.custom.localhost';
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
|
||||
$this->assertEquals(307, $response['headers']['status-code']);
|
||||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('type', ['redirect'])->toString(),
|
||||
Query::equal('trigger', ['manual'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
|
||||
$this->cleanupSite($siteId);
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testCreateFunctionRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-function.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$setup = $this->setupFunction();
|
||||
$functionId = $setup['functionId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($functionId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupFunctionRule($domain, $functionId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
|
||||
$this->assertEventually(function () use ($functionId, $deploymentId) {
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['function'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$functionId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentId', [$deploymentId])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
});
|
||||
}
|
||||
|
||||
public function testCreateSiteRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-site.custom.localhost';
|
||||
|
||||
$proxyClient = new Client();
|
||||
$proxyClient->setEndpoint('http://appwrite.test');
|
||||
$proxyClient->addHeader('x-appwrite-hostname', $domain);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupSiteRule($domain, $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertSame(200, $rule['headers']['status-code']);
|
||||
$this->assertSame('created', $rule['body']['status']);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('Contact page', $response['body']);
|
||||
|
||||
// Wildcard domains automatically get verified status
|
||||
$domains = [
|
||||
\uniqid() . '.sites.localhost',
|
||||
\uniqid() . '.rebranded.localhost',
|
||||
];
|
||||
foreach ($domains as $domain) {
|
||||
$wildcardRuleId = $this->setupSiteRule($domain, $siteId);
|
||||
$this->assertNotEmpty($wildcardRuleId);
|
||||
$rule = $this->getRule($wildcardRuleId);
|
||||
$this->assertSame(200, $rule['headers']['status-code']);
|
||||
$this->assertSame('verified', $rule['body']['status']);
|
||||
$this->cleanupRule($wildcardRuleId);
|
||||
}
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('trigger', ['deployment'])->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertGreaterThan(0, $rules['body']['total']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupSite($siteId);
|
||||
|
||||
$this->assertEventually(function () use ($siteId, $deploymentId) {
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
Query::equal('type', ['deployment'])->toString(),
|
||||
Query::equal('deploymentId', [$deploymentId])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
});
|
||||
}
|
||||
|
||||
public function testCreateSiteBranchRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-site-branch.custom.localhost';
|
||||
|
||||
$setup = $this->setupSite();
|
||||
$siteId = $setup['siteId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($siteId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testCreateFunctionBranchRule(): void
|
||||
{
|
||||
$domain = \uniqid() . '-function-branch.custom.localhost';
|
||||
|
||||
$setup = $this->setupFunction();
|
||||
$functionId = $setup['functionId'];
|
||||
$deploymentId = $setup['deploymentId'];
|
||||
|
||||
$this->assertNotEmpty($functionId);
|
||||
$this->assertNotEmpty($deploymentId);
|
||||
|
||||
$ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
}
|
||||
|
||||
public function testUpdateRule(): void
|
||||
{
|
||||
// Create function appwrite-network domain
|
||||
$functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0];
|
||||
$domain = \uniqid() . '-cname-api.' . $functionsDomain;
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// Create site appwrite-network domain
|
||||
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
|
||||
$domain = \uniqid() . '-cname-api.' . $sitesDomain;
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verified', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// Create + update
|
||||
$domain = \uniqid() . '-cname-api.custom.com';
|
||||
|
||||
$rule = $this->createAPIRule($domain);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testGetRule()
|
||||
{
|
||||
$domain = \uniqid() . '-get.custom.localhost';
|
||||
$ruleId = $this->setupAPIRule($domain);
|
||||
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals($domain, $rule['body']['domain']);
|
||||
$this->assertEquals('manual', $rule['body']['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rule['body']);
|
||||
$this->assertArrayHasKey('domain', $rule['body']);
|
||||
$this->assertArrayHasKey('type', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectUrl', $rule['body']);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
|
||||
$this->assertArrayHasKey('logs', $rule['body']);
|
||||
$this->assertArrayHasKey('renewAt', $rule['body']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
public function testListRules()
|
||||
{
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$rule = $this->deleteRule($rule['$id']);
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
|
||||
$rule1Domain = \uniqid() . '-list1.custom.localhost';
|
||||
$rule1Id = $this->setupAPIRule($rule1Domain);
|
||||
$this->assertNotEmpty($rule1Id);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(1, $rules['body']['total']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
|
||||
|
||||
$this->assertEquals('manual', $rules['body']['rules'][0]['trigger']);
|
||||
$this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('domain', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('type', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
|
||||
$this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
|
||||
|
||||
$rule2Domain = \uniqid() . '-list1.custom.localhost';
|
||||
$rule2Id = $this->setupAPIRule($rule2Domain);
|
||||
$this->assertNotEmpty($rule2Id);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
$this->assertCount(2, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::limit(1)->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('$id', [$rule1Id])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::orderDesc('$id')->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(2, $rules['body']['rules']);
|
||||
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('domain', [$rule2Domain])->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertCount(1, $rules['body']['rules']);
|
||||
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule1Domain,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleIds = \array_column($rules['body']['rules'], '$id');
|
||||
$this->assertContains($rule1Id, $ruleIds);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule2Domain,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleIds = \array_column($rules['body']['rules'], '$id');
|
||||
$this->assertContains($rule2Id, $ruleIds);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule1Id,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
|
||||
$this->assertContains($rule1Domain, $ruleDomains);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'search' => $rule2Id,
|
||||
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
|
||||
$this->assertContains($rule2Domain, $ruleDomains);
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
foreach ($rules['body']['rules'] as $rule) {
|
||||
$rule = $this->deleteRule($rule['$id']);
|
||||
$this->assertEquals(204, $rule['headers']['status-code']);
|
||||
}
|
||||
|
||||
$rules = $this->listRules();
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(0, $rules['body']['total']);
|
||||
$this->assertCount(0, $rules['body']['rules']);
|
||||
}
|
||||
|
||||
public function testRuleVerification(): void
|
||||
{
|
||||
|
||||
// 1. Site rule can verify
|
||||
$site = $this->setupSite();
|
||||
$siteId = $site['siteId'];
|
||||
|
||||
$rule = $this->createSiteRule('stage-site.webapp.com', $siteId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
$this->assertNotEmpty($rule['body']['$id']);
|
||||
$ruleId = $rule['body']['$id'];
|
||||
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals($ruleId, $rule['body']['$id']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
$this->cleanupSite($siteId);
|
||||
|
||||
// 2. Function rule can verify
|
||||
$function = $this->setupFunction();
|
||||
$functionId = $function['functionId'];
|
||||
|
||||
$rule = $this->createFunctionRule('stage-function.webapp.com', $functionId);
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$rule = $this->createAPIRule('stage-site.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
$this->cleanupFunction($functionId);
|
||||
|
||||
// 3. Wrong A record fails to verify
|
||||
$rule = $this->createAPIRule('wrong-a-webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 4. Correct A record can verify
|
||||
$rule = $this->createAPIRule('webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// 5. Correct CNAME record can verify (no CAA record)
|
||||
$rule = $this->createAPIRule('stage.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
|
||||
// 6. Missing CNAME record fails to verify
|
||||
$rule = $this->createAPIRule('stage-missing-cname.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 7. Wrong CNAME record fails to verify
|
||||
$rule = $this->createAPIRule('stage-wrong-cname.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 8. Wrong CAA record fails to verify
|
||||
$rule = $this->createAPIRule('stage-wrong-caa.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$rule = $this->updateRuleVerification($ruleId);
|
||||
$this->assertEquals(400, $rule['headers']['status-code']);
|
||||
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']);
|
||||
|
||||
$rule = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
|
||||
// 9. Correct CAA record can verify
|
||||
$rule = $this->createAPIRule('stage-correct-caa.webapp.com');
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('verifying', $rule['body']['status']);
|
||||
$this->assertEmpty($rule['body']['logs']);
|
||||
|
||||
$this->cleanupRule($rule['body']['$id']);
|
||||
}
|
||||
|
||||
public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void
|
||||
{
|
||||
$domain = \uniqid() . '-timestamp-test.webapp.com';
|
||||
$rule = $this->createAPIRule($domain);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
$this->assertEquals('created', $rule['body']['status']);
|
||||
$this->assertNotEmpty($rule['body']['logs']);
|
||||
|
||||
$ruleId = $rule['body']['$id'];
|
||||
$initialUpdatedAt = $rule['body']['$updatedAt'];
|
||||
$initiallogs = $rule['body']['logs'];
|
||||
|
||||
sleep(1);
|
||||
|
||||
$updatedRule = $this->updateRuleVerification($ruleId);
|
||||
|
||||
$this->assertEquals(400, $updatedRule['headers']['status-code']);
|
||||
$this->assertStringContainsString($initiallogs, $updatedRule['body']['message']);
|
||||
|
||||
$ruleAfterUpdate = $this->getRule($ruleId);
|
||||
$this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']);
|
||||
$this->assertEquals('created', $ruleAfterUpdate['body']['status']);
|
||||
$this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']);
|
||||
$this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']);
|
||||
|
||||
$initialTime = new \DateTime($initialUpdatedAt);
|
||||
$updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']);
|
||||
$this->assertGreaterThan($initialTime, $updatedTime);
|
||||
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Proxy;
|
||||
|
||||
use Appwrite\ID;
|
||||
use Appwrite\Tests\Async;
|
||||
use CURLFile;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Console;
|
||||
|
||||
trait ProxyHelpers
|
||||
{
|
||||
use Async;
|
||||
|
||||
protected function listRules(array $params = []): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createAPIRule(string $domain): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function updateRuleStatus(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/status', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'siteId' => $siteId,
|
||||
'branch' => $branch,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function getRule(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'url' => $url,
|
||||
'statusCode' => $statusCode,
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain,
|
||||
'functionId' => $functionId,
|
||||
'branch' => $branch,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function deleteRule(string $ruleId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
protected function setupAPIRule(string $domain): string
|
||||
{
|
||||
$rule = $this->createAPIRule($domain);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
|
||||
{
|
||||
$rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
|
||||
{
|
||||
$rule = $this->createFunctionRule($domain, $functionId, $branch);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
|
||||
{
|
||||
$rule = $this->createSiteRule($domain, $siteId, $branch);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function cleanupRule(string $ruleId): void
|
||||
{
|
||||
$rule = $this->deleteRule($ruleId);
|
||||
$this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
|
||||
}
|
||||
|
||||
protected function cleanupSite(string $siteId): void
|
||||
{
|
||||
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
|
||||
}
|
||||
|
||||
protected function cleanupFunction(string $functionId): void
|
||||
{
|
||||
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
|
||||
}
|
||||
|
||||
protected function setupSite(): mixed
|
||||
{
|
||||
// Site
|
||||
$site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'siteId' => ID::unique(),
|
||||
'name' => 'Proxy site',
|
||||
'framework' => 'other',
|
||||
'adapter' => 'static',
|
||||
'buildRuntime' => 'static-1',
|
||||
'outputDirectory' => './',
|
||||
'buildCommand' => '',
|
||||
'installCommand' => '',
|
||||
'fallbackFile' => '',
|
||||
]);
|
||||
|
||||
$this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
$siteId = $site['body']['$id'];
|
||||
|
||||
// Deployment
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'code' => $this->packageSite('static'),
|
||||
'activate' => 'true'
|
||||
]);
|
||||
|
||||
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
|
||||
$deploymentId = $deployment['body']['$id'] ?? '';
|
||||
|
||||
$this->assertEventually(function () use ($siteId, $deploymentId) {
|
||||
$site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
|
||||
}, 120000, 500);
|
||||
|
||||
return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
|
||||
}
|
||||
|
||||
protected function setupFunction(): mixed
|
||||
{
|
||||
// Function
|
||||
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'functionId' => ID::unique(),
|
||||
'runtime' => 'node-22',
|
||||
'name' => 'Proxy Function',
|
||||
'entrypoint' => 'index.js',
|
||||
'commands' => '',
|
||||
'execute' => ['any']
|
||||
]);
|
||||
|
||||
$this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
// Deployment
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'code' => $this->packageFunction('basic'),
|
||||
'activate' => 'true'
|
||||
]);
|
||||
|
||||
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
|
||||
$deploymentId = $deployment['body']['$id'] ?? '';
|
||||
|
||||
$this->assertEventually(function () use ($functionId, $deploymentId) {
|
||||
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
}, 100000, 500);
|
||||
|
||||
return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
|
||||
}
|
||||
|
||||
private function packageSite(string $site): CURLFile
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
|
||||
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
|
||||
$tarPath = "$folderPath/code.tar.gz";
|
||||
|
||||
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
|
||||
|
||||
if (filesize($tarPath) > 1024 * 1024 * 5) {
|
||||
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
|
||||
}
|
||||
|
||||
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
|
||||
}
|
||||
|
||||
private function packageFunction(string $function): CURLFile
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
|
||||
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
|
||||
$tarPath = "$folderPath/code.tar.gz";
|
||||
|
||||
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
|
||||
|
||||
if (filesize($tarPath) > 1024 * 1024 * 5) {
|
||||
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
|
||||
}
|
||||
|
||||
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\TablesDB;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ApiTablesDB;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\SchemaPolling;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
use Tests\E2E\Traits\DatabasesUrlHelpers;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class DatabasesNumericTypesTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
use ApiTablesDB;
|
||||
use DatabasesUrlHelpers;
|
||||
use SchemaPolling;
|
||||
|
||||
private static array $setupCache = [];
|
||||
|
||||
/**
|
||||
* Setup database, table, and numeric columns for parallel-safe tests.
|
||||
*/
|
||||
protected function setupDatabaseAndTable(): array
|
||||
{
|
||||
$cacheKey = $this->getProject()['$id'] ?? 'default';
|
||||
if (!empty(self::$setupCache[$cacheKey])) {
|
||||
return self::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$apiKey = $this->getProject()['apiKey'];
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $apiKey,
|
||||
];
|
||||
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Numeric Types Test Database',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'Numeric Types Table',
|
||||
'rowSecurity' => true,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $table['headers']['status-code']);
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
// Create integer column
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
|
||||
'key' => 'integer_field',
|
||||
'required' => false,
|
||||
'min' => -10,
|
||||
'max' => 10,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
// Create bigint column
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
|
||||
'key' => 'bigint_field',
|
||||
'required' => false,
|
||||
'min' => -9007199254740991,
|
||||
'max' => 9007199254740991,
|
||||
'default' => 9007199254740000,
|
||||
]);
|
||||
|
||||
// Create unsigned integer column
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
|
||||
'key' => 'unsigned_int_field',
|
||||
'required' => false,
|
||||
'min' => 0,
|
||||
'max' => 100,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
]);
|
||||
|
||||
// Create unsigned bigint column
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
|
||||
'key' => 'unsigned_bigint_field',
|
||||
'required' => false,
|
||||
'min' => 0,
|
||||
'max' => 9223372036854775807,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
]);
|
||||
|
||||
// Cache before waiting so that if waitForAllAttributes times out,
|
||||
// subsequent calls don't try to re-create the same columns (causing 409)
|
||||
self::$setupCache[$cacheKey] = [
|
||||
'databaseId' => $databaseId,
|
||||
'tableId' => $tableId,
|
||||
];
|
||||
|
||||
// Wait for all columns to be available
|
||||
$this->waitForAllAttributes($databaseId, $tableId);
|
||||
|
||||
return self::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup database/table without caching so mutations (update/delete) don't
|
||||
* affect other tests that might be executed in a different order.
|
||||
*/
|
||||
protected function setupFreshDatabaseAndTable(): array
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$apiKey = $this->getProject()['apiKey'];
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $apiKey,
|
||||
];
|
||||
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Numeric Types Test Database',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'Numeric Types Table',
|
||||
'rowSecurity' => true,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $table['headers']['status-code']);
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
|
||||
'key' => 'integer_field',
|
||||
'required' => false,
|
||||
'min' => -10,
|
||||
'max' => 10,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
|
||||
'key' => 'bigint_field',
|
||||
'required' => false,
|
||||
'min' => -9007199254740991,
|
||||
'max' => 9007199254740991,
|
||||
'default' => 9007199254740000,
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
|
||||
'key' => 'unsigned_int_field',
|
||||
'required' => false,
|
||||
'max' => 100,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
|
||||
'key' => 'unsigned_bigint_field',
|
||||
'required' => false,
|
||||
'max' => 9223372036854775807,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
]);
|
||||
|
||||
$this->waitForAllAttributes($databaseId, $tableId);
|
||||
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'tableId' => $tableId,
|
||||
];
|
||||
}
|
||||
|
||||
public function testCreateDatabase(): void
|
||||
{
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Numeric Types Test Database',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateTable(): void
|
||||
{
|
||||
$data = $this->setupDatabaseAndTable();
|
||||
|
||||
$table = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $data['databaseId'] . '/tables/' . $data['tableId'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $table['headers']['status-code']);
|
||||
$this->assertEquals($data['tableId'], $table['body']['$id']);
|
||||
}
|
||||
|
||||
public function testGetIntegerAndBigIntColumns(): void
|
||||
{
|
||||
$data = $this->setupDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
$integerColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $integerColumn['headers']['status-code']);
|
||||
$this->assertEquals('integer_field', $integerColumn['body']['key']);
|
||||
$this->assertEquals('integer', $integerColumn['body']['type']);
|
||||
$this->assertEquals(false, $integerColumn['body']['required']);
|
||||
$this->assertEquals(false, $integerColumn['body']['array']);
|
||||
$this->assertEquals(-10, $integerColumn['body']['min']);
|
||||
$this->assertEquals(10, $integerColumn['body']['max']);
|
||||
$this->assertEquals(0, $integerColumn['body']['default']);
|
||||
|
||||
$bigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $bigintColumn['headers']['status-code']);
|
||||
$this->assertEquals('bigint_field', $bigintColumn['body']['key']);
|
||||
|
||||
$this->assertEquals('bigint', $bigintColumn['body']['type']);
|
||||
$this->assertEquals(false, $bigintColumn['body']['required']);
|
||||
$this->assertEquals(false, $bigintColumn['body']['array']);
|
||||
$this->assertEquals(-9007199254740991, $bigintColumn['body']['min']);
|
||||
$this->assertEquals(9007199254740991, $bigintColumn['body']['max']);
|
||||
$this->assertEquals(9007199254740000, $bigintColumn['body']['default']);
|
||||
}
|
||||
|
||||
public function testGetUnsignedIntegerAndBigIntColumns(): void
|
||||
{
|
||||
$data = $this->setupDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
$unsignedIntColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_int_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $unsignedIntColumn['headers']['status-code']);
|
||||
$this->assertEquals('unsigned_int_field', $unsignedIntColumn['body']['key']);
|
||||
$this->assertEquals('integer', $unsignedIntColumn['body']['type']);
|
||||
$this->assertEquals(false, $unsignedIntColumn['body']['required']);
|
||||
$this->assertEquals(false, $unsignedIntColumn['body']['array']);
|
||||
$this->assertEquals(false, $unsignedIntColumn['body']['signed']);
|
||||
$this->assertEquals(0, $unsignedIntColumn['body']['min']);
|
||||
$this->assertEquals(100, $unsignedIntColumn['body']['max']);
|
||||
$this->assertEquals(0, $unsignedIntColumn['body']['default']);
|
||||
|
||||
$unsignedBigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_bigint_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $unsignedBigintColumn['headers']['status-code']);
|
||||
$this->assertEquals('unsigned_bigint_field', $unsignedBigintColumn['body']['key']);
|
||||
$this->assertEquals('bigint', $unsignedBigintColumn['body']['type']);
|
||||
$this->assertEquals(false, $unsignedBigintColumn['body']['required']);
|
||||
$this->assertEquals(false, $unsignedBigintColumn['body']['array']);
|
||||
$this->assertEquals(false, $unsignedBigintColumn['body']['signed']);
|
||||
$this->assertEquals(0, $unsignedBigintColumn['body']['min']);
|
||||
$this->assertEquals(9223372036854775807, $unsignedBigintColumn['body']['max']);
|
||||
$this->assertEquals(0, $unsignedBigintColumn['body']['default']);
|
||||
}
|
||||
|
||||
public function testListColumnsWithNumericTypes(): void
|
||||
{
|
||||
$data = $this->setupDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
$columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $columns['headers']['status-code']);
|
||||
$this->assertIsArray($columns['body']['columns']);
|
||||
$this->assertGreaterThan(0, $columns['body']['total']);
|
||||
|
||||
$columnKeys = array_map(fn ($col) => $col['key'], $columns['body']['columns']);
|
||||
$this->assertContains('integer_field', $columnKeys);
|
||||
$this->assertContains('bigint_field', $columnKeys);
|
||||
$this->assertContains('unsigned_int_field', $columnKeys);
|
||||
$this->assertContains('unsigned_bigint_field', $columnKeys);
|
||||
|
||||
$columnTypeByKey = [];
|
||||
foreach ($columns['body']['columns'] as $col) {
|
||||
$columnTypeByKey[$col['key']] = $col['type'];
|
||||
}
|
||||
|
||||
$this->assertEquals('integer', $columnTypeByKey['integer_field']);
|
||||
$this->assertEquals('bigint', $columnTypeByKey['bigint_field']);
|
||||
$this->assertEquals('integer', $columnTypeByKey['unsigned_int_field']);
|
||||
$this->assertEquals('bigint', $columnTypeByKey['unsigned_bigint_field']);
|
||||
}
|
||||
|
||||
public function testCreateRowWithIntegerAndBigIntTypes(): void
|
||||
{
|
||||
$data = $this->setupDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
$row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'rowId' => ID::unique(),
|
||||
'data' => [
|
||||
'integer_field' => 5,
|
||||
'bigint_field' => 456,
|
||||
'unsigned_int_field' => 50,
|
||||
'unsigned_bigint_field' => 9007199254740000,
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $row['headers']['status-code']);
|
||||
$this->assertEquals(5, $row['body']['integer_field']);
|
||||
$this->assertEquals(456, $row['body']['bigint_field']);
|
||||
$this->assertEquals(50, $row['body']['unsigned_int_field']);
|
||||
$this->assertEquals(9007199254740000, $row['body']['unsigned_bigint_field']);
|
||||
}
|
||||
|
||||
public function testUpdateIntegerAndBigIntColumns(): void
|
||||
{
|
||||
$data = $this->setupFreshDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
// Update integer column
|
||||
$updateInteger = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer/integer_field',
|
||||
[
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
],
|
||||
[
|
||||
'required' => false,
|
||||
'min' => -20,
|
||||
'max' => 20,
|
||||
'default' => 3,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $updateInteger['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $tableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $column['headers']['status-code']);
|
||||
$this->assertEquals(-20, $column['body']['min']);
|
||||
$this->assertEquals(20, $column['body']['max']);
|
||||
$this->assertEquals(3, $column['body']['default']);
|
||||
}, 30000, 250);
|
||||
|
||||
// Update bigint column
|
||||
$updateBigint = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint/bigint_field',
|
||||
[
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
],
|
||||
[
|
||||
'required' => false,
|
||||
'min' => -999,
|
||||
'max' => 999,
|
||||
'default' => 10,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $updateBigint['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $tableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $column['headers']['status-code']);
|
||||
$this->assertEquals(-999, $column['body']['min']);
|
||||
$this->assertEquals(999, $column['body']['max']);
|
||||
$this->assertEquals(10, $column['body']['default']);
|
||||
}, 30000, 250);
|
||||
}
|
||||
|
||||
public function testDeleteIntegerAndBigIntColumns(): void
|
||||
{
|
||||
$data = $this->setupFreshDatabaseAndTable();
|
||||
$databaseId = $data['databaseId'];
|
||||
$tableId = $data['tableId'];
|
||||
|
||||
// Delete integer column
|
||||
$deleteInteger = $this->client->call(
|
||||
Client::METHOD_DELETE,
|
||||
'/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field',
|
||||
[
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(204, $deleteInteger['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $tableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $column['headers']['status-code']);
|
||||
}, 30000, 250);
|
||||
|
||||
// Delete bigint column
|
||||
$deleteBigint = $this->client->call(
|
||||
Client::METHOD_DELETE,
|
||||
'/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field',
|
||||
[
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(204, $deleteBigint['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $tableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $column['headers']['status-code']);
|
||||
}, 30000, 250);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user