Merge branch '1.9.x' into revert-11585-revert-11402-sync-mongodb

This commit is contained in:
Jake Barnby
2026-03-24 03:03:02 +00:00
committed by GitHub
38 changed files with 1974 additions and 299 deletions
+2 -1
View File
@@ -394,7 +394,8 @@ jobs:
Webhooks,
VCS,
Messaging,
Migrations
Migrations,
Project
]
include:
- service: Databases
+2
View File
@@ -62,6 +62,8 @@ $admins = [
'devKeys.write',
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'locale.read',
'avatars.read',
'health.read',
-8
View File
@@ -31,12 +31,4 @@ return [
"description" =>
"Access to create, update, and delete project\'s development keys",
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
+8
View File
@@ -180,4 +180,12 @@ return [ // List of publicly visible scopes
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
"project.read" => [
"description" =>
"Access to read project\'s information",
],
"project.write" => [
"description" =>
"Access to update project\'s information",
],
];
+29 -2
View File
@@ -209,6 +209,22 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
$oauthProvider = null;
try {
$jwtDecoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
$payload = $jwtDecoder->decode($secret);
if (empty($payload['provider'])) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$oauthProvider = $payload['provider'];
$secret = $payload['secret'];
} catch (\Ahc\Jwt\JWTException) {
// Not a JWT — use secret as-is (non-OAuth flows)
}
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
@@ -220,6 +236,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
?: $userFromRequest->tokenVerify(null, $secret, $proofForCode);
if (!$verifiedToken) {
// Could mean invalid/expired JWT, or expired secret
throw new Exception(Exception::USER_INVALID_TOKEN);
}
// OAuth2 tokens must have a provider from the JWT
if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_OAUTH2 && $oauthProvider === null) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@@ -245,7 +267,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2,
TOKEN_TYPE_OAUTH2 => $oauthProvider,
default => SESSION_PROVIDER_TOKEN,
};
$session = new Document(array_merge(
@@ -1899,7 +1921,12 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setParam('tokenId', $token->getId())
;
$query['secret'] = $secret;
// Wrap secret in a JWT that also carries the provider name
$jwtEncoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
$query['secret'] = $jwtEncoder->encode([
'secret' => $secret,
'provider' => $provider,
]);
$query['userId'] = $user->getId();
// If the `token` param is not set, we persist the session in a cookie
-235
View File
@@ -1,25 +1,15 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
Http::get('/v1/project/usage')
@@ -442,228 +432,3 @@ Http::get('/v1/project/usage')
'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0,
]), Response::MODEL_USAGE_PROJECT);
});
// Variables
Http::post('/v1/project/variables')
->desc('Create variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('audits.event', 'variable.create')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'createVariable',
description: '/docs/references/project/create-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
]
))
->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 projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variableId = ID::unique();
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => '',
'resourceId' => '',
'resourceType' => 'project',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $key, 'project']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::get('/v1/project/variables')
->desc('List variables')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'listVariables',
description: '/docs/references/project/list-variables.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
]
))
->inject('response')
->inject('dbForProject')
->action(function (Response $response, Database $dbForProject) {
$variables = $dbForProject->find('variables', [
Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY)
]);
$response->dynamic(new Document([
'variables' => $variables,
'total' => \count($variables),
]), Response::MODEL_VARIABLE_LIST);
});
Http::get('/v1/project/variables/:variableId')
->desc('Get variable')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'getVariable',
description: '/docs/references/project/get-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::put('/v1/project/variables/:variableId')
->desc('Update variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateVariable',
description: '/docs/references/project/update-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->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('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)
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable->getAttribute('secret') === true && $secret === false) {
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, $key, 'project']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::delete('/v1/project/variables/:variableId')
->desc('Delete variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'deleteVariable',
description: '/docs/references/project/delete-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('project')
->inject('response')
->inject('dbForProject')
->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response->noContent();
});
+3 -3
View File
@@ -13,9 +13,9 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test --config pint.json",
"format": "vendor/bin/pint --config pint.json",
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"bench": "vendor/bin/phpbench run --report=benchmark",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
},
@@ -84,7 +84,7 @@
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "2.*",
"utopia-php/vcs": "3.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+8 -7
View File
@@ -5216,22 +5216,23 @@
},
{
"name": "utopia-php/vcs",
"version": "2.0.2",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "5769679308bad498f2777547d48ab332166c4c0b"
"reference": "0efe842d695acb4b184f5306a836169c771fbcea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
"reference": "5769679308bad498f2777547d48ab332166c4c0b",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/0efe842d695acb4b184f5306a836169c771fbcea",
"reference": "0efe842d695acb4b184f5306a836169c771fbcea",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
"utopia-php/cache": "1.0.*"
"utopia-php/cache": "1.0.*",
"utopia-php/fetch": "0.5.*"
},
"require-dev": {
"laravel/pint": "1.*.*",
@@ -5258,9 +5259,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/2.0.2"
"source": "https://github.com/utopia-php/vcs/tree/3.0.1"
},
"time": "2026-03-13T15:25:16+00:00"
"time": "2026-03-23T15:58:31+00:00"
},
{
"name": "utopia-php/websocket",
+1
View File
@@ -37,6 +37,7 @@
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
<directory>./tests/e2e/Services/Project</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
+16 -11
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Event;
use Appwrite\Messaging\Adapter;
use Appwrite\Messaging\Adapter\Realtime as RealtimeAdapter;
use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception;
@@ -96,17 +97,21 @@ class Realtime extends Event
: [$target['projectId'] ?? $this->getProject()->getId()];
foreach ($projectIds as $projectId) {
$this->realtime->send(
projectId: $projectId,
payload: $this->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $this->getParam('userId')
]
);
try {
$this->realtime->send(
projectId: $projectId,
payload: $this->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $this->getParam('userId')
]
);
} catch (\Exception $e) {
Console::error('Realtime send failed: '.$e->getMessage());
}
}
return true;
+2
View File
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Project;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
@@ -38,5 +39,6 @@ class Appwrite extends Platform
$this->addModule(new Storage\Module());
$this->addModule(new VCS\Module());
$this->addModule(new Webhooks\Module());
$this->addModule(new Project\Module());
}
}
@@ -0,0 +1,32 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Platform\Action;
class Init extends Action
{
public static function getName(): string
{
return 'init';
}
public function __construct()
{
$this
->setType(Action::TYPE_INIT)
->groups(['project'])
->inject('project')
->callback(function (Document $project) {
if ($project->getId() === 'console') {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
}
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
});
}
}
@@ -0,0 +1,108 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\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;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createProjectVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/variables')
->desc('Create project variable')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'variables.[variableId].create')
->label('audits.event', 'project.variable.create')
->label('audits.resource', 'project.variable/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
name: 'createVariable',
description: <<<EOT
Create a new project environment variable. These variables can be accessed by all functions and sites in the project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
],
))
->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.')
->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)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(
string $variableId,
string $key,
string $value,
bool $secret,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject,
) {
$variableId = ($variableId == 'unique()') ? ID::unique() : $variableId;
$variable = new Document([
'$id' => $variableId,
'$permissions' => [],
'resourceInternalId' => '', // Already in project DB anyway
'resourceId' => '', // Already in project DB anyway
'resourceType' => 'project',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $key, 'project']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
foreach (['functions', 'sites'] as $collection) {
$dbForProject->updateDocuments($collection, new Document([
'live' => false
]));
}
$queueForEvents->setParam('variableId', $variable->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,88 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteProjectVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/project/variables/:variableId')
->desc('Delete project variable')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'variables.[variableId].delete')
->label('audits.event', 'project.variable.delete')
->label('audits.resource', 'project.variable/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
name: 'deleteVariable',
description: <<<EOT
Delete a variable by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $variableId,
Response $response,
Database $dbForProject,
Event $queueForEvents,
) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('variables', $variable->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
};
foreach (['functions', 'sites'] as $collection) {
$dbForProject->updateDocuments($collection, new Document([
'live' => false
]));
}
$queueForEvents->setParam('variableId', $variable->getId());
$response->noContent();
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
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\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getProjectVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/variables/:variableId')
->desc('Get project variable')
->groups(['api', 'project'])
->label('scope', 'project.read')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
name: 'getVariable',
description: <<<EOT
Get a variable by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(
string $variableId,
Response $response,
Database $dbForProject,
) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,121 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\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\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateProjectVariable';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/variables/:variableId')
->desc('Update project variable')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'variables.[variableId].update')
->label('audits.event', 'project.variable.update')
->label('audits.resource', 'project.variable/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
name: 'updateVariable',
description: <<<EOT
Update variable by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable 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)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(
string $variableId,
?string $key,
?string $value,
?bool $secret,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject,
) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$isSecretVariable = $variable->getAttribute('secret', false) === true;
if ($isSecretVariable && $secret === false) {
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
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, $key, 'project']));
}
if (!\is_null($value)) {
$updates->setAttribute('value', $value);
}
if (!\is_null($secret)) {
$updates->setAttribute('secret', $secret);
}
try {
$variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
} catch (Duplicate $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
foreach (['functions', 'sites'] as $collection) {
$dbForProject->updateDocuments($collection, new Document([
'live' => false
]));
}
$queueForEvents->setParam('variableId', $variable->getId());
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
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\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\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listProjectVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/variables')
->desc('List project variables')
->groups(['api', 'project'])
->label('scope', 'project.read')
->label('sdk', new Method(
namespace: 'project',
group: 'variables',
name: 'listVariables',
description: <<<EOT
Get a list of all project environment variables.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
]
))
->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('project')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Document $project,
Response $response,
Database $dbForProject,
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('resourceType', ['project']);
$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', ['project']),
]);
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' => $variables,
'total' => $total,
]), Response::MODEL_VARIABLE_LIST);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Project;
use Appwrite\Platform\Modules\Project\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,29 @@
<?php
namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Update as UpdateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\XList as ListVariables;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Hooks
$this->addAction(Init::getName(), new Init());
// Project
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
}
}
@@ -85,14 +85,16 @@ class Get extends Action
$repository = $github->getRepository($owner, $repositoryName);
$authorized = false;
try {
$installationRepository = $github->getInstallationRepository($repositoryName);
if (!empty($installationRepository)) {
$authorized = true;
$authorized = $github->hasAccessToAllRepositories();
if (!$authorized) {
try {
$installationRepository = $github->getInstallationRepository($repositoryName);
if (!empty($installationRepository)) {
$authorized = true;
}
} catch (RepositoryNotFound $e) {
$authorized = false;
}
} catch (RepositoryNotFound $e) {
$authorized = false;
}
$repository['id'] = \strval($repository['id']) ?? '';
@@ -141,7 +141,7 @@ class XList extends Action
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
['items' => $repos, 'total' => $total] = $github->searchRepositories($providerInstallationId, $owner, $page, $limit, $search);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
+3 -1
View File
@@ -362,7 +362,9 @@ class Migrations extends Action
'targets.read',
'targets.write',
'webhooks.read',
'webhooks.write'
'webhooks.write',
'project.read',
'project.write'
]
]);
@@ -7,7 +7,8 @@ class Variables extends Base
public const ALLOWED_ATTRIBUTES = [
'key',
'resourceType',
'resourceId'
'resourceId',
'secret',
];
/**
@@ -2,6 +2,7 @@
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Query;
use Appwrite\Utopia\Request\Filter;
class V21 extends Filter
@@ -13,6 +14,12 @@ class V21 extends Filter
case 'webhooks.create':
$content = $this->fillWebhookid($content);
break;
case 'project.createVariable':
$content = $this->fillVariableId($content);
break;
case 'project.listVariables':
$content = $this->preserveVariablesQueries($content);
break;
case 'functions.createTemplateDeployment':
case 'sites.createTemplateDeployment':
$content = $this->convertVersionToTypeAndReference($content);
@@ -57,4 +64,19 @@ class V21 extends Filter
$content['webhookId'] = $content['webhookId'] ?? 'unique()';
return $content;
}
protected function fillVariableId(array $content): array
{
$content['variableId'] = $content['variableId'] ?? 'unique()';
return $content;
}
protected function preserveVariablesQueries(array $content): array
{
$content['queries'] = $content['queries'] ?? [
Query::limit(APP_LIMIT_SUBQUERY)
];
return $content;
}
}
+2
View File
@@ -163,6 +163,8 @@ trait ProjectCustom
'tokens.write',
'webhooks.read',
'webhooks.write',
'project.read',
'project.write'
],
]);
@@ -2182,11 +2182,138 @@ class AccountCustomClientTest extends Scope
]), [
'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
]);
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2', $response['headers']['location']);
$oauthClient = new Client();
$oauthClient->setEndpoint('');
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/callback/mock/' . $this->getProject()['$id'] . '?code=', $response['headers']['location']);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/mock/redirect?code=', $response['headers']['location']);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertArrayHasKey('a_session_' . $this->getProject()['$id'] . '_legacy', $response['cookies']);
$this->assertArrayHasKey('a_session_' . $this->getProject()['$id'], $response['cookies']);
$oauthUserCookie = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertNotEmpty($oauthUserCookie);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
// Ensure user is authenticated
$response = $this->client->call(Client::METHOD_GET, '/account', [
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('useroauth@localhost.test', $response['body']['email']);
$oauthUserId = $response['body']['$id'];
$this->assertNotEmpty($oauthUserId);
// Ensure session looks as expected
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', [
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($oauthUserId, $response['body']['userId']);
$this->assertEquals('mock', $response['body']['provider']);
// Same sign-in again, but this time with oauth2 token flow
$response = $this->client->call(Client::METHOD_GET, '/account/tokens/oauth2/' . $provider, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2', $response['headers']['location']);
$oauthClient = new Client();
$oauthClient->setEndpoint('');
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/callback/mock/' . $this->getProject()['$id'] . '?code=', $response['headers']['location']);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/mock/redirect?code=', $response['headers']['location']);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2/success?secret=', $response['headers']['location']);
$oauthParamsString = \parse_url($response['headers']['location'], PHP_URL_QUERY);
$oauthParams = [];
\parse_str($oauthParamsString, $oauthParams);
$this->assertNotEmpty($oauthParams['secret']);
$this->assertNotEmpty($oauthParams['userId']);
$response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
// Claim session
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => $oauthParams['userId'],
'secret' => $oauthParams['secret'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('mock', $response['body']['provider']);
$this->assertArrayHasKey('a_session_' . $this->getProject()['$id'] . '_legacy', $response['cookies']);
$this->assertArrayHasKey('a_session_' . $this->getProject()['$id'], $response['cookies']);
$oauthUserCookie = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertNotEmpty($oauthUserCookie);
$response = $this->client->call(Client::METHOD_GET, '/account', [
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('useroauth@localhost.test', $response['body']['email']);
$oauthUserId = $response['body']['$id'];
$this->assertNotEmpty($oauthUserId);
// Ensure session looks as expected
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', [
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($oauthUserId, $response['body']['userId']);
$this->assertEquals('mock', $response['body']['provider']);
/**
* Test for Failure when disabled
*/
@@ -264,7 +264,7 @@ trait FunctionsBase
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
@@ -991,7 +991,7 @@ class FunctionsCustomServerTest extends Scope
*/
$folder = 'large';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
$chunkSize = 5 * 1024 * 1024;
$handle = @fopen($code, "rb");
+1 -1
View File
@@ -3464,7 +3464,7 @@ trait Base
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
@@ -1188,7 +1188,7 @@ trait MigrationsBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class VariablesConsoleClientTest extends Scope
{
use VariablesBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class VariablesCustomServerTest extends Scope
{
use VariablesBase;
use ProjectCustom;
use SideServer;
}
@@ -83,7 +83,7 @@ class WebhooksCustomServerTest extends Scope
$stdout = '';
$folder = 'timeout';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
// Create variable first
$this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
@@ -734,7 +734,7 @@ class WebhooksCustomServerTest extends Scope
$stdout = '';
$folder = 'timeout';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
@@ -274,6 +274,7 @@ trait ProjectsBase
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -288,6 +289,7 @@ trait ProjectsBase
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -4686,6 +4686,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -4702,6 +4703,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -4720,6 +4722,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE',
'value' => 'ANOTHERTESTINGVALUE'
]);
@@ -4732,6 +4735,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => str_repeat("A", 256),
'value' => 'TESTINGVALUE'
]);
@@ -4744,6 +4748,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'LONGKEY',
'value' => str_repeat("#", 8193),
]);
@@ -4889,18 +4894,6 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains("APP_TEST_UPDATE", $variableKeys);
$this->assertContains("APP_TEST_UPDATE_1", $variableKeys);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()));
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
@@ -4909,6 +4902,19 @@ class ProjectsConsoleClientTest extends Scope
'value' => 'TESTINGVALUEUPDATED_2'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertSame('TESTINGVALUEUPDATED_2', $response['body']['value']);
$this->assertSame('APP_TEST_UPDATE', $response['body']['key']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()));
$this->assertEquals(400, $response['headers']['status-code']);
$longKey = str_repeat("A", 256);
@@ -4958,6 +4964,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_DELETE',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -4972,6 +4979,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'variableId' => 'unique()',
'key' => 'APP_TEST_DELETE_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
+2 -2
View File
@@ -271,7 +271,7 @@ trait ProxyBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
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.');
@@ -288,7 +288,7 @@ trait ProxyBase
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
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.');
+1 -1
View File
@@ -241,7 +241,7 @@ trait SitesBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');