Merge branch '1.8.x' into chore-usecases-enums

This commit is contained in:
Matej Bačo
2026-01-29 12:12:17 +01:00
committed by GitHub
41 changed files with 1403 additions and 354 deletions
+2 -2
View File
@@ -26,8 +26,8 @@ unset($common['files']);
$collections = [
'buckets' => $buckets,
'databases' => $databases,
'projects' => array_merge($projects, $common),
'console' => array_merge($platform, $common),
'projects' => array_merge_recursive($projects, $common),
'console' => array_merge_recursive($platform, $common),
'logs' => $logs,
];
+7 -36
View File
@@ -633,29 +633,7 @@ $platformCollections = [
'name' => 'keys',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'resourceType',
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -666,7 +644,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => 'resourceId',
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -677,7 +655,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => 'resourceInternalId',
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -756,21 +734,14 @@ $platformCollections = [
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resource',
'$id' => ID::custom('_key_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType', 'resourceInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_accessedAt',
'$id' => ID::custom('_key_accessedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
+25
View File
@@ -357,6 +357,11 @@ return [
'description' => 'API key and session used in the same request. Use either `setSession` or `setKey`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::USER_JWT_AND_COOKIE_SET => [
'name' => Exception::USER_JWT_AND_COOKIE_SET,
'description' => 'JWT and cookie used in the same request. Use either `setJWT` or `setCookie`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::API_KEY_EXPIRED => [
'name' => Exception::API_KEY_EXPIRED,
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
@@ -1074,6 +1079,11 @@ return [
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ACCOUNT_KEY_EXPIRED => [
'name' => Exception::ACCOUNT_KEY_EXPIRED,
'description' => 'The account API key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ROUTER_HOST_NOT_FOUND => [
'name' => Exception::ROUTER_HOST_NOT_FOUND,
'description' => 'Host is not trusted. This could occur because you have not configured a custom domain. Add a custom domain to your project first and try again.',
@@ -1328,4 +1338,19 @@ return [
'description' => 'Target has an invalid provider type.',
'code' => 400,
],
Exception::USER_ID_MISSING => [
'name' => Exception::USER_ID_MISSING,
'description' => 'When using account API key, make sure to pass x-appwrite-user header with your user ID.',
'code' => 403,
],
Exception::ORGANIZATION_ID_MISSING => [
'name' => Exception::ORGANIZATION_ID_MISSING,
'description' => 'When using organization API key, make sure to pass x-appwrite-organization header with your organization ID.',
'code' => 403,
],
Exception::PROJECT_ID_MISSING => [
'name' => Exception::PROJECT_ID_MISSING,
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
'code' => 403,
],
];
+15
View File
@@ -0,0 +1,15 @@
<?php
// List of scopes for Account API keys (Tokens)
return [
"account" => [
"description" => 'Access to manage account, its organizations, sessions, tokens, and billing.',
],
"teams.read" => [
"description" => 'Access to read account\'s organizations.',
],
"teams.write" => [
"description" => 'Access to create, update and delete account\'s organizations and its memberships.',
],
];
+42
View File
@@ -0,0 +1,42 @@
<?php
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
"projects.write" => [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
"devKeys.write" => [
"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",
],
];
+21 -1
View File
@@ -227,7 +227,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '13.0.1',
'version' => '13.1.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -250,6 +250,26 @@ return [
],
],
],
[
'key' => 'markdown',
'name' => 'Markdown',
'version' => '0.1.0',
'url' => 'https://github.com/appwrite/sdk-for-md.git',
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'markdown',
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
'gitRepoName' => 'sdk-for-md',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
],
],
],
+3 -6
View File
@@ -1483,7 +1483,7 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', null, new Nullable(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.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1502,9 +1502,6 @@ App::post('/v1/projects/:projectId/keys')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
@@ -1628,7 +1625,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('scopes', null, new Nullable(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 . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1729,7 +1726,7 @@ App::post('/v1/projects/:projectId/jwts')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
->inject('response')
->inject('dbForPlatform')
+8 -1
View File
@@ -433,15 +433,22 @@ App::delete('/v1/teams/:teamId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
}
// Sync delete
$deletes = new Deletes();
$deletes->deleteMemberships($getProjectDB, $team, $project);
// Async delete
if ($project->getId() === 'console') {
$queueForDeletes
->setType(DELETE_TYPE_TEAM_PROJECTS)
->setDocument($team);
->setDocument($team)
->trigger();
}
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($team);
$queueForEvents
->setParam('teamId', $team->getId())
->setPayload($response->output($team, Response::MODEL_TEAM))
+4 -4
View File
@@ -1228,7 +1228,7 @@ App::error()
}
switch ($class) {
case 'Utopia\Exception':
case Utopia\Exception::class:
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
switch ($code) {
case 400:
@@ -1239,10 +1239,10 @@ App::error()
break;
}
break;
case 'Utopia\Database\Exception\Authorization':
case Utopia\Database\Exception\Authorization::class:
$error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED);
break;
case 'Utopia\Database\Exception\Timeout':
case Utopia\Database\Exception\Timeout::class:
$error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error);
break;
}
@@ -1503,7 +1503,7 @@ App::error()
$template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null);
// TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment
if (!\str_starts_with($route->getPath(), '/v1')) {
if (empty($route) || !\str_starts_with($route->getPath(), '/v1')) {
$template = __DIR__ . '/../views/general/error.phtml';
}
+1 -4
View File
@@ -191,7 +191,7 @@ App::post('/v1/mock/api-key-unprefixed')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$scopes = array_keys(Config::getParam('scopes'));
$scopes = array_keys(Config::getParam('projectScopes'));
$key = new Document([
'$id' => ID::unique(),
@@ -200,9 +200,6 @@ App::post('/v1/mock/api-key-unprefixed')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
+38 -18
View File
@@ -157,10 +157,6 @@ App::init()
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
@@ -189,23 +185,38 @@ App::init()
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
$dbKey = null;
if (!empty($apiKey->getProjectId())) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getUserId())) {
$dbKey = $user->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getTeamId())) {
$dbKey = $team->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
}
if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$updates = new Document();
$accessedAt = $dbKey->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$dbKey->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('accessedAt', DateTime::now());
}
$sdkValidator = new WhiteList($servers, true);
@@ -216,12 +227,21 @@ App::init()
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$dbKey->setAttribute('sdks', $sdks);
/** Update access time as well */
$dbKey->setAttribute('accessedAt', Datetime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('sdks', $sdks);
$updates->setAttribute('accessedAt', Datetime::now());
}
}
if (!$updates->isEmpty()) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
if (!empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (!empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (!empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
}
+3 -1
View File
@@ -22,7 +22,9 @@ Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapt
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);
Config::load('usage', __DIR__ . '/../config/usage.php', $configAdapter);
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
Config::load('projectScopes', __DIR__ . '/../config/scopes/project.php', $configAdapter);
Config::load('organizationScopes', __DIR__ . '/../config/scopes/organization.php', $configAdapter);
Config::load('accountScopes', __DIR__ . '/../config/scopes/account.php', $configAdapter);
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
+3
View File
@@ -194,6 +194,7 @@ const DELETE_TYPE_SITES = 'sites';
const DELETE_TYPE_FUNCTIONS = 'functions';
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAMS = 'teams';
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
@@ -249,6 +250,8 @@ const MESSAGE_TYPE_PUSH = 'push';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_DYNAMIC = 'dynamic';
const API_KEY_ORGANIZATION = 'organization';
const API_KEY_ACCOUNT = 'account';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
+31
View File
@@ -433,3 +433,34 @@ Database::addFilter(
return $value;
}
);
Database::addFilter(
'subQueryOrganizationKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['teams']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryAccountKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['users']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
+1 -1
View File
@@ -177,7 +177,7 @@ Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_L
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
+62 -4
View File
@@ -343,6 +343,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
* overwriting the previous value.
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
*/
$authorization->setDefaultStatus(true);
@@ -419,12 +420,17 @@ App::setResource('user', function (string $mode, Document $project, Document $co
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@@ -440,6 +446,34 @@ App::setResource('user', function (string $mode, Document $project, Document $co
}
}
}
// Account based on account API key
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
@@ -1227,7 +1261,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request);
$path = $route->getPath();
$path = !empty($route) ? $route->getPath() : $request->getURI();
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
@@ -1282,15 +1316,39 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
return '';
}, ['request', 'apiKey']);
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
App::setResource('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
return null;
}
return Key::decode($project, $key);
}, ['request', 'project']);
$key = Key::decode($project, $team, $user, $key);
$userHeader = $request->getHeader('x-appwrite-user');
$organizationHeader = $request->getHeader('x-appwrite-organization');
$projectHeader = $request->getHeader('x-appwrite-project');
if (!empty($key->getProjectId())) {
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
throw new Exception(Exception::PROJECT_ID_MISSING);
}
}
if (!empty($key->getUserId())) {
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
throw new Exception(Exception::USER_ID_MISSING);
}
}
if (!empty($key->getTeamId())) {
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
}
}
return $key;
}, ['request', 'project', 'team', 'user']);
App::setResource('executor', fn () => new Executor());
Generated
+164 -101
View File
@@ -798,6 +798,68 @@
},
"time": "2026-01-12T17:58:43+00:00"
},
{
"name": "halaxa/json-machine",
"version": "1.2.6",
"source": {
"type": "git",
"url": "https://github.com/halaxa/json-machine.git",
"reference": "8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/halaxa/json-machine/zipball/8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4",
"reference": "8bf0b0ff6ff60ab480778eaa5ad7d505b442c2d4",
"shasum": ""
},
"require": {
"php": "7.2 - 8.5"
},
"require-dev": {
"ext-json": "*",
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^8.0"
},
"suggest": {
"ext-json": "To run JSON Machine out of the box without custom decoders.",
"guzzlehttp/guzzle": "To run example with GuzzleHttp"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"JsonMachine\\": "src/"
},
"exclude-from-classmap": [
"src/autoloader.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Filip Halaxa",
"email": "filip@halaxa.cz"
}
],
"description": "Efficient, easy-to-use and fast JSON pull parser",
"support": {
"issues": "https://github.com/halaxa/json-machine/issues",
"source": "https://github.com/halaxa/json-machine/tree/1.2.6"
},
"funding": [
{
"url": "https://ko-fi.com/G2G57KTE4",
"type": "other"
}
],
"time": "2025-12-05T14:53:09+00:00"
},
{
"name": "league/csv",
"version": "9.24.1",
@@ -1236,16 +1298,16 @@
},
{
"name": "open-telemetry/api",
"version": "1.7.1",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/api.git",
"reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4"
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
"reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
"shasum": ""
},
"require": {
@@ -1255,7 +1317,7 @@
"symfony/polyfill-php82": "^1.26"
},
"conflict": {
"open-telemetry/sdk": "<=1.0.8"
"open-telemetry/sdk": "<=1.11"
},
"type": "library",
"extra": {
@@ -1302,7 +1364,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-10-19T10:49:48+00:00"
"time": "2026-01-21T04:14:03+00:00"
},
{
"name": "open-telemetry/context",
@@ -1492,16 +1554,16 @@
},
{
"name": "open-telemetry/sdk",
"version": "1.11.0",
"version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sdk.git",
"reference": "d91f21addcdb42da9a451c002777f8318432461a"
"reference": "7f1bd524465c1ca42755a9ef1143ba09913f5be0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/d91f21addcdb42da9a451c002777f8318432461a",
"reference": "d91f21addcdb42da9a451c002777f8318432461a",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/7f1bd524465c1ca42755a9ef1143ba09913f5be0",
"reference": "7f1bd524465c1ca42755a9ef1143ba09913f5be0",
"shasum": ""
},
"require": {
@@ -1542,7 +1604,7 @@
]
},
"branch-alias": {
"dev-main": "1.9.x-dev"
"dev-main": "1.12.x-dev"
}
},
"autoload": {
@@ -1585,20 +1647,20 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2026-01-15T11:21:03+00:00"
"time": "2026-01-21T04:14:03+00:00"
},
{
"name": "open-telemetry/sem-conv",
"version": "1.37.0",
"version": "1.38.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sem-conv.git",
"reference": "8da7ec497c881e39afa6657d72586e27efbd29a1"
"reference": "e613bc640a407def4991b8a936a9b27edd9a3240"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1",
"reference": "8da7ec497c881e39afa6657d72586e27efbd29a1",
"url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/e613bc640a407def4991b8a936a9b27edd9a3240",
"reference": "e613bc640a407def4991b8a936a9b27edd9a3240",
"shasum": ""
},
"require": {
@@ -1638,11 +1700,11 @@
],
"support": {
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
"docs": "https://opentelemetry.io/docs/php",
"docs": "https://opentelemetry.io/docs/languages/php",
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-09-03T12:08:10+00:00"
"time": "2026-01-21T04:14:03+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@@ -2004,16 +2066,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.48",
"version": "3.0.49",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9",
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9",
"shasum": ""
},
"require": {
@@ -2094,7 +2156,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.49"
},
"funding": [
{
@@ -2110,7 +2172,7 @@
"type": "tidelift"
}
],
"time": "2025-12-15T11:51:42+00:00"
"time": "2026-01-27T09:17:28+00:00"
},
{
"name": "psr/container",
@@ -2673,16 +2735,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.3",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616"
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616",
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616",
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": ""
},
"require": {
@@ -2750,7 +2812,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.3"
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
},
"funding": [
{
@@ -2770,7 +2832,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:50:43+00:00"
"time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3553,16 +3615,16 @@
},
{
"name": "utopia-php/audit",
"version": "2.0.4",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "1301ab2607667b9f86456f86895f3e26f8c0c9a7"
"reference": "8e0540aa939968418ee3ad2b2c305992a771e142"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/1301ab2607667b9f86456f86895f3e26f8c0c9a7",
"reference": "1301ab2607667b9f86456f86895f3e26f8c0c9a7",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/8e0540aa939968418ee3ad2b2c305992a771e142",
"reference": "8e0540aa939968418ee3ad2b2c305992a771e142",
"shasum": ""
},
"require": {
@@ -3596,9 +3658,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/2.0.4"
"source": "https://github.com/utopia-php/audit/tree/2.1.0"
},
"time": "2026-01-14T07:22:46+00:00"
"time": "2026-01-22T12:40:48+00:00"
},
{
"name": "utopia-php/auth",
@@ -3899,16 +3961,16 @@
},
{
"name": "utopia-php/database",
"version": "4.5.2",
"version": "4.6.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23"
"reference": "53394759c44067e9db4660635765e2056f83788c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"url": "https://api.github.com/repos/utopia-php/database/zipball/53394759c44067e9db4660635765e2056f83788c",
"reference": "53394759c44067e9db4660635765e2056f83788c",
"shasum": ""
},
"require": {
@@ -3951,9 +4013,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/4.5.2"
"source": "https://github.com/utopia-php/database/tree/4.6.2"
},
"time": "2026-01-15T04:23:30+00:00"
"time": "2026-01-22T07:14:12+00:00"
},
{
"name": "utopia-php/detector",
@@ -4516,22 +4578,23 @@
},
{
"name": "utopia-php/migration",
"version": "1.4.4",
"version": "1.4.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "3fe751902012d09d323420cd3523be1ed855e868"
"reference": "f358db6fb6a01d855bbed39e283387069e4f277d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/3fe751902012d09d323420cd3523be1ed855e868",
"reference": "3fe751902012d09d323420cd3523be1ed855e868",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/f358db6fb6a01d855bbed39e283387069e4f277d",
"reference": "f358db6fb6a01d855bbed39e283387069e4f277d",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*",
"ext-curl": "*",
"ext-openssl": "*",
"halaxa/json-machine": "^1.2",
"php": ">=8.1",
"utopia-php/console": "0.0.*",
"utopia-php/database": "4.*",
@@ -4565,9 +4628,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.4.4"
"source": "https://github.com/utopia-php/migration/tree/1.4.6"
},
"time": "2026-01-16T10:00:07+00:00"
"time": "2026-01-20T11:07:17+00:00"
},
{
"name": "utopia-php/mongo",
@@ -5057,28 +5120,28 @@
},
{
"name": "utopia-php/swoole",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/swoole.git",
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b"
"reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/95a937acb393dbf95cccba239d55886e2848ab0b",
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95",
"reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.0",
"ext-swoole": "6.*",
"php": ">=8.1",
"utopia-php/framework": "0.33.37"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3",
"swoole/ide-helper": "5.0.2"
"swoole/ide-helper": "6.0.2"
},
"type": "library",
"autoload": {
@@ -5102,9 +5165,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/1.0.0"
"source": "https://github.com/utopia-php/swoole/tree/1.0.1"
},
"time": "2026-01-14T14:00:11+00:00"
"time": "2026-01-28T12:43:38+00:00"
},
{
"name": "utopia-php/system",
@@ -5482,16 +5545,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.8.17",
"version": "1.8.21",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "1bc5a39bf87d3c2064f2f8d45fa712340338bc41"
"reference": "1b47b2c794811c565f8b5e7eeaa19f749bcbeb6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1bc5a39bf87d3c2064f2f8d45fa712340338bc41",
"reference": "1bc5a39bf87d3c2064f2f8d45fa712340338bc41",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1b47b2c794811c565f8b5e7eeaa19f749bcbeb6b",
"reference": "1b47b2c794811c565f8b5e7eeaa19f749bcbeb6b",
"shasum": ""
},
"require": {
@@ -5527,9 +5590,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.8.17"
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.21"
},
"time": "2026-01-19T12:13:41+00:00"
"time": "2026-01-26T04:42:33+00:00"
},
{
"name": "doctrine/annotations",
@@ -6709,16 +6772,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.31",
"version": "9.6.34",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "945d0b7f346a084ce5549e95289962972c4272e5"
"reference": "b36f02317466907a230d3aa1d34467041271ef4a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5",
"reference": "945d0b7f346a084ce5549e95289962972c4272e5",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
"reference": "b36f02317466907a230d3aa1d34467041271ef4a",
"shasum": ""
},
"require": {
@@ -6740,7 +6803,7 @@
"phpunit/php-timer": "^5.0.3",
"sebastian/cli-parser": "^1.0.2",
"sebastian/code-unit": "^1.0.8",
"sebastian/comparator": "^4.0.9",
"sebastian/comparator": "^4.0.10",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.8",
@@ -6792,7 +6855,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
},
"funding": [
{
@@ -6816,7 +6879,7 @@
"type": "tidelift"
}
],
"time": "2025-12-06T07:45:52+00:00"
"time": "2026-01-27T05:45:00+00:00"
},
{
"name": "psr/cache",
@@ -7036,16 +7099,16 @@
},
{
"name": "sebastian/comparator",
"version": "4.0.9",
"version": "4.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5"
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
"shasum": ""
},
"require": {
@@ -7098,7 +7161,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9"
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
},
"funding": [
{
@@ -7118,7 +7181,7 @@
"type": "tidelift"
}
],
"time": "2025-08-10T06:51:50+00:00"
"time": "2026-01-24T09:22:56+00:00"
},
{
"name": "sebastian/complexity",
@@ -7976,16 +8039,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587"
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"shasum": ""
},
"require": {
@@ -8042,7 +8105,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.3"
"source": "https://github.com/symfony/console/tree/v8.0.4"
},
"funding": [
{
@@ -8062,7 +8125,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-13T13:06:50+00:00"
},
{
"name": "symfony/filesystem",
@@ -8136,16 +8199,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.3",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12"
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"shasum": ""
},
"require": {
@@ -8180,7 +8243,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.3"
"source": "https://github.com/symfony/finder/tree/v8.0.5"
},
"funding": [
{
@@ -8200,7 +8263,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8605,16 +8668,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.3",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78"
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78",
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"shasum": ""
},
"require": {
@@ -8646,7 +8709,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.3"
"source": "https://github.com/symfony/process/tree/v8.0.5"
},
"funding": [
{
@@ -8666,20 +8729,20 @@
"type": "tidelift"
}
],
"time": "2025-12-19T10:01:18+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/string",
"version": "v8.0.1",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc"
"reference": "758b372d6882506821ed666032e43020c4f57194"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"shasum": ""
},
"require": {
@@ -8736,7 +8799,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.1"
"source": "https://github.com/symfony/string/tree/v8.0.4"
},
"funding": [
{
@@ -8756,7 +8819,7 @@
"type": "tidelift"
}
],
"time": "2025-12-01T09:13:36+00:00"
"time": "2026-01-12T12:37:40+00:00"
},
{
"name": "textalk/websocket",
@@ -9012,5 +9075,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+1
View File
@@ -98,6 +98,7 @@ services:
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- mariadb
- redis
@@ -0,0 +1 @@
appwrite health get-queue-audits
+24
View File
@@ -1,5 +1,29 @@
# Change Log
## 13.1.0
- Mark `appwrite generate` command as stable
- Improve permissions param to be a typesafe callback
- Fix relationship handling in generated code
- Fix `appwrite client` properly hanlding `--key` parameter
- Fix `init site` not working on Windows
## 13.1.0-rc.3
- Allow generation of server side CRUD operations on databases and tables
- Fix npm distribution failing due to missing template files in bundle
## 13.1.0-rc.2
- Update generated `databases` services to automatically initialize a client instance
- Update generator to use handlebars templates
## 13.1.0-rc.1
- Feat: `appwrite generate` command to create a fully typesafe SDK for your Appwrite project
- Chore: improve creation of columns during table creation by passing them directly instead of creating them one by one
- Improved config validation by adding extra rules in zod schema
## 13.0.1
- Fix `project init` command leading to Cannot convert to BigInt error
+5
View File
@@ -0,0 +1,5 @@
# Change Log
## 0.1.0
* Initial release
+93 -3
View File
@@ -15,6 +15,8 @@ class Key
{
public function __construct(
protected string $projectId,
protected string $teamId,
protected string $userId,
protected string $type,
protected string $role,
protected array $scopes,
@@ -34,6 +36,16 @@ class Key
return $this->projectId;
}
public function getUserId(): string
{
return $this->userId;
}
public function getTeamId(): string
{
return $this->teamId;
}
public function getType(): string
{
return $this->type;
@@ -95,13 +107,12 @@ class Key
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
* Can be a stored API key or a dynamic key (JWT).
*
* @param Document $project
* @param string $key
* @return Key
* @throws Exception
*/
public static function decode(
Document $project,
Document $team,
Document $user,
string $key
): Key {
if (\str_contains($key, '_')) {
@@ -118,6 +129,8 @@ class Key
$guestKey = new Key(
$project->getId(),
'',
'',
$type,
User::ROLE_GUESTS,
$roles[User::ROLE_GUESTS]['scopes'] ?? [],
@@ -133,6 +146,7 @@ class Key
leeway: 0
);
$payload = [];
try {
$payload = $jwtObj->decode($secret);
} catch (JWTException) {
@@ -155,6 +169,8 @@ class Key
return new Key(
$projectId,
'',
'',
$type,
$role,
$scopes,
@@ -188,12 +204,86 @@ class Key
return new Key(
$project->getId(),
'',
'',
$type,
$role,
$scopes,
$name,
$expired
);
case API_KEY_ACCOUNT:
$key = $user->find(
key: 'secret',
find: $key,
subject: 'keys'
);
// Invalid key
if (!$key) {
return $guestKey;
}
$expire = $key->getAttribute('expire');
$expired = false;
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
$expired = true;
}
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_USERS;
$scopes = $key->getAttribute('scopes', []);
$key = new Key(
'',
'',
$user->getId(),
$type,
$role,
$scopes,
$name,
$expired
);
return $key;
case API_KEY_ORGANIZATION:
$key = $team->find(
key: 'secret',
find: $key,
subject: 'keys'
);
// Invalid key
if (!$key) {
return $guestKey;
}
$expire = $key->getAttribute('expire');
$expired = false;
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
$expired = true;
}
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_APPS;
$scopes = $key->getAttribute('scopes', []);
$key = new Key(
'',
$team->getId(),
'',
$type,
$role,
$scopes,
$name,
$expired
);
return $key;
default:
return $guestKey;
}
+7 -1
View File
@@ -107,7 +107,9 @@ class Exception extends \Exception
public const string USER_DELETION_PROHIBITED = 'user_deletion_prohibited';
public const string USER_TARGET_NOT_FOUND = 'user_target_not_found';
public const string USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
public const string USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set';
public const string USER_API_KEY_AND_SESSION_SET = 'user_api_key_and_session_set';
public const string USER_JWT_AND_COOKIE_SET = 'user_jwt_and_cookie_set';
public const string USER_ID_MISSING = 'user_id_missing';
public const string API_KEY_EXPIRED = 'api_key_expired';
@@ -119,6 +121,8 @@ class Exception extends \Exception
public const string TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
public const string TEAM_ALREADY_EXISTS = 'team_already_exists';
public const string ORGANIZATION_ID_MISSING = 'organization_id_missing';
/** Console */
public const string RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
@@ -283,6 +287,7 @@ class Exception extends \Exception
/** Projects */
public const string PROJECT_NOT_FOUND = 'project_not_found';
public const string PROJECT_ID_MISSING = 'project_id_missing';
public const string PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
public const string PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
public const string PROJECT_ALREADY_EXISTS = 'project_already_exists';
@@ -290,6 +295,7 @@ class Exception extends \Exception
public const string PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
public const string PROJECT_RESERVED_PROJECT = 'project_reserved_project';
public const string PROJECT_KEY_EXPIRED = 'project_key_expired';
public const string ACCOUNT_KEY_EXPIRED = 'account_key_expired';
public const string PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
+53 -53
View File
@@ -269,59 +269,59 @@ class Mapper
}
switch ((!empty($validator)) ? $validator::class : '') {
case 'Appwrite\Auth\Validator\Password':
case 'Appwrite\Event\Validator\Event':
case 'Appwrite\Event\Validator\FunctionEvent':
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Network\Validator\Email':
case 'Appwrite\Network\Validator\Redirect':
case 'Appwrite\Network\Validator\DNS':
case 'Appwrite\Network\Validator\Origin':
case 'Appwrite\Task\Validator\Cron':
case 'Appwrite\Utopia\Database\Validator\CustomId':
case 'Utopia\Database\Validator\Key':
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\Domain':
case 'Utopia\Validator\HexColor':
case 'Utopia\Validator\Host':
case 'Utopia\Validator\IP':
case 'Utopia\Validator\Origin':
case 'Utopia\Validator\Text':
case 'Utopia\Validator\URL':
case 'Utopia\Validator\WhiteList':
case \Appwrite\Auth\Validator\Password::class:
case \Appwrite\Event\Validator\Event::class:
case \Appwrite\Event\Validator\FunctionEvent::class:
case \Appwrite\Network\Validator\CNAME::class:
case \Appwrite\Network\Validator\Email::class:
case \Appwrite\Network\Validator\Redirect::class:
case \Appwrite\Network\Validator\DNS::class:
case \Appwrite\Network\Validator\Origin::class:
case \Appwrite\Task\Validator\Cron::class:
case \Appwrite\Utopia\Database\Validator\CustomId::class:
case \Utopia\Database\Validator\Key::class:
case \Utopia\Database\Validator\UID::class:
case \Utopia\Validator\Domain::class:
case \Utopia\Validator\HexColor::class:
case \Utopia\Validator\Host::class:
case \Utopia\Validator\IP::class:
case \Utopia\Validator\Origin::class:
case \Utopia\Validator\Text::class:
case \Utopia\Validator\URL::class:
case \Utopia\Validator\WhiteList::class:
default:
$type = Type::string();
break;
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Tables':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Columns':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
case 'Utopia\Database\Validator\Authorization':
case 'Utopia\Database\Validator\Permissions':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Utopia\Database\Validator\Roles':
case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Base::class:
case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class:
case \Appwrite\Utopia\Database\Validator\Queries\Tables::class:
case \Appwrite\Utopia\Database\Validator\Queries\Collections::class:
case \Appwrite\Utopia\Database\Validator\Queries\Columns::class:
case \Appwrite\Utopia\Database\Validator\Queries\Databases::class:
case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class:
case \Appwrite\Utopia\Database\Validator\Queries\Executions::class:
case \Appwrite\Utopia\Database\Validator\Queries\Files::class:
case \Appwrite\Utopia\Database\Validator\Queries\Functions::class:
case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Installations::class:
case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class:
case \Appwrite\Utopia\Database\Validator\Queries\Projects::class:
case \Appwrite\Utopia\Database\Validator\Queries\Rules::class:
case \Appwrite\Utopia\Database\Validator\Queries\Teams::class:
case \Appwrite\Utopia\Database\Validator\Queries\Users::class:
case \Appwrite\Utopia\Database\Validator\Queries\Variables::class:
case \Utopia\Database\Validator\Authorization::class:
case \Utopia\Database\Validator\Permissions::class:
case \Utopia\Database\Validator\Queries::class:
case \Utopia\Database\Validator\Queries\Documents::class:
case \Utopia\Database\Validator\Roles::class:
$type = Type::listOf(Type::string());
break;
case 'Utopia\Validator\Boolean':
case \Utopia\Validator\Boolean::class:
$type = Type::boolean();
break;
case 'Utopia\Validator\ArrayList':
case \Utopia\Validator\ArrayList::class:
$type = Type::listOf(self::param(
$utopia,
$validator->getValidator(),
@@ -329,11 +329,11 @@ class Mapper
$injections
));
break;
case 'Utopia\Validator\Integer':
case 'Utopia\Validator\Numeric':
case \Utopia\Validator\Integer::class:
case \Utopia\Validator\Numeric::class:
$type = Type::int();
break;
case 'Utopia\Validator\Range':
case \Utopia\Validator\Range::class:
// Check if the Range validator is for float or integer
if ($validator instanceof \Utopia\Validator\Range && $validator->getType() === \Utopia\Validator\Range::TYPE_FLOAT) {
$type = Type::float();
@@ -341,16 +341,16 @@ class Mapper
$type = Type::int();
}
break;
case 'Utopia\Validator\FloatValidator':
case \Utopia\Validator\FloatValidator::class:
$type = Type::float();
break;
case 'Utopia\Validator\Assoc':
case \Utopia\Validator\Assoc::class:
$type = Types::assoc();
break;
case 'Utopia\Validator\JSON':
case \Utopia\Validator\JSON::class:
$type = Types::json();
break;
case 'Utopia\Storage\Validator\File':
case \Utopia\Storage\Validator\File::class:
$type = Types::inputFile();
break;
}
@@ -6,6 +6,7 @@ use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
@@ -250,6 +251,35 @@ abstract class Action extends DatabasesAction
return $document;
}
/**
* Validate relationship values.
* Handles Document objects, ID strings, and associative arrays.
*/
protected function validateRelationship(mixed $relation): void
{
$relationId = null;
if ($relation instanceof Document) {
$relationId = $relation->getId();
} elseif (\is_string($relation)) {
$relationId = $relation;
} elseif (\is_array($relation) && !\array_is_list($relation)) {
$relationId = $relation['$id'] ?? null;
} else {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object, document ID string, or associative array');
}
if ($relationId !== null) {
if (!\is_string($relationId)) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship $id must be a string');
}
$validator = new CustomId();
if (!$validator->isValid($relationId)) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $validator->getDescription());
}
}
}
/**
* Resolves relationships in a document and attaches metadata.
*/
@@ -319,6 +319,9 @@ class Create extends Action
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
$this->validateRelationship($relation);
if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
@@ -210,6 +210,9 @@ class Update extends Action
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
$this->validateRelationship($relation);
if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
@@ -220,6 +220,9 @@ class Upsert extends Action
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
$this->validateRelationship($relation);
if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
@@ -87,7 +87,7 @@ class Create extends Base
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
@@ -83,7 +83,7 @@ class Update extends Base
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true)
->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
+29
View File
@@ -12,6 +12,7 @@ use Appwrite\SDK\Language\Flutter;
use Appwrite\SDK\Language\Go;
use Appwrite\SDK\Language\GraphQL;
use Appwrite\SDK\Language\Kotlin;
use Appwrite\SDK\Language\Markdown;
use Appwrite\SDK\Language\Node;
use Appwrite\SDK\Language\PHP;
use Appwrite\SDK\Language\Python;
@@ -31,6 +32,27 @@ use Utopia\Validator\WhiteList;
class SDKs extends Action
{
protected array $supportedSDKS = [
'web',
'cli',
'php',
'nodejs',
'deno',
'python',
'ruby',
'flutter',
'react-native',
'dart',
'go',
'swift',
'apple',
'dotnet',
'android',
'graphql',
'rest',
'markdown',
];
public static function getName(): string
{
return 'sdks';
@@ -61,6 +83,9 @@ class SDKs extends Action
if (!$sdks) {
$selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
if ($selectedSDK !== '*' && !\in_array($selectedSDK, $this->supportedSDKS)) {
throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $this->supportedSDKS));
}
} else {
$sdks = explode(',', $sdks);
}
@@ -252,6 +277,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
case 'rest':
$config = new REST();
break;
case 'markdown':
$config = new Markdown();
$config->setNPMPackage('@appwrite.io/docs');
break;
default:
throw new \Exception('Language "' . $language['key'] . '" not supported');
}
+1 -1
View File
@@ -190,7 +190,7 @@ class Screenshot extends Action
'cookie' => $cookieConsole
], [
'name' => 'Screenshot API key',
'scopes' => \array_keys(Config::getParam('scopes', []))
'scopes' => \array_keys(Config::getParam('projectScopes', []))
]);
if ($response['headers']['status-code'] !== 201) {
@@ -276,7 +276,7 @@ class Certificates extends Action
]));
// Rule not found (or) not in the expected state
if ($rule->isEmpty() || $rule->getAttribute('status') !== RULE_STATUS_CERTIFICATE_GENERATING) {
if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) {
Console::warning('Certificate generation for ' . $domain->get() . ' is skipped as the associated rule is either empty or not in the expected state.');
return;
}
+31 -1
View File
@@ -125,6 +125,9 @@ class Deletes extends Action
case DELETE_TYPE_USERS:
$this->deleteUser($getProjectDB, $document, $project);
break;
case DELETE_TYPE_TEAMS:
$this->deleteTeam($getProjectDB, $document, $project);
break;
case DELETE_TYPE_BUCKETS:
$this->deleteBucket($getProjectDB, $deviceForFiles, $document, $project);
break;
@@ -530,7 +533,7 @@ class Deletes extends Action
}
/**
* @var $dbForProject Database
* @var Database $dbForProject
*/
$dbForProject = $getProjectDB($document);
@@ -661,6 +664,24 @@ class Deletes extends Action
}
}
private function deleteTeam(callable $getProjectDB, Document $document, Document $project): void
{
$teamId = $document->getId();
$teamInternalId = $document->getSequence();
$dbForProject = $getProjectDB($project);
if ($project->getId() === 'console') {
// Delete Keys
$this->deleteByGroup('keys', [
Query::equal('resourceInternalId', [$teamInternalId]),
Query::equal('resourceType', ['teams']),
Query::orderAsc()
], $dbForProject);
}
$dbForProject->purgeCachedDocument('teams', $teamId);
}
/**
* @param callable $getProjectDB
* @param Document $document user document
@@ -680,6 +701,15 @@ class Deletes extends Action
Query::orderAsc()
], $dbForProject);
if ($project->getId() === 'console') {
// Delete Keys
$this->deleteByGroup('keys', [
Query::equal('resourceInternalId', [$userInternalId]),
Query::equal('resourceType', ['users']),
Query::orderAsc()
], $dbForProject);
}
$dbForProject->purgeCachedDocument('users', $userId);
// Delete Memberships and decrement team membership counts
@@ -392,51 +392,51 @@ class OpenAPI3 extends Format
: '';
switch ($base) {
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case \Appwrite\Utopia\Database\Validator\Queries\Base::class:
$class = $base;
break;
}
if ($class === 'Utopia\Validator\AnyOf') {
if ($class === \Utopia\Validator\AnyOf::class) {
$validator = $param['validator']->getValidators()[0];
$class = \get_class($validator);
}
$array = false;
if ($class === 'Utopia\Validator\ArrayList') {
if ($class === \Utopia\Validator\ArrayList::class) {
$array = true;
$subclass = \get_class($validator->getValidator());
switch ($subclass) {
case 'Appwrite\Utopia\Database\Validator\Operation':
case 'Utopia\Validator\WhiteList':
case \Appwrite\Utopia\Database\Validator\Operation::class:
case \Utopia\Validator\WhiteList::class:
$class = $subclass;
break;
}
}
switch ($class) {
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\Text':
case \Utopia\Database\Validator\UID::class:
case \Utopia\Validator\Text::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Validator\Boolean':
case \Utopia\Validator\Boolean::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: false;
break;
case 'Appwrite\Utopia\Database\Validator\CustomId':
case \Appwrite\Utopia\Database\Validator\CustomId::class:
if ($sdk->getType() === MethodType::UPLOAD) {
$node['schema']['x-upload-id'] = true;
}
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Database\Validator\DatetimeValidator':
case \Utopia\Database\Validator\DatetimeValidator::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'datetime';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
break;
case 'Utopia\Database\Validator\Spatial':
case \Utopia\Database\Validator\Spatial::class:
/** @var Spatial $validator */
$node['schema']['type'] = 'array';
$node['schema']['items'] = [
@@ -450,31 +450,31 @@ class OpenAPI3 extends Format
Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]',
};
break;
case 'Appwrite\Network\Validator\Email':
case \Appwrite\Network\Validator\Email::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'email';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'email@example.com';
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Appwrite\Network\Validator\Redirect':
case \Utopia\Validator\Host::class:
case \Utopia\Validator\URL::class:
case \Appwrite\Network\Validator\Redirect::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'url';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '{}';
break;
case 'Utopia\Storage\Validator\File':
case \Utopia\Storage\Validator\File::class:
$consumes = ['multipart/form-data'];
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'binary';
break;
case 'Utopia\Validator\ArrayList':
case \Utopia\Validator\ArrayList::class:
/** @var ArrayList $validator */
$node['schema']['type'] = 'array';
$node['schema']['items'] = [
@@ -484,92 +484,92 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = $param['example'];
}
break;
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Columns':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Tables':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Identities':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Appwrite\Utopia\Database\Validator\Queries\Messages':
case 'Appwrite\Utopia\Database\Validator\Queries\Migrations':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Appwrite\Utopia\Database\Validator\Queries\Providers':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Subscribers':
case 'Appwrite\Utopia\Database\Validator\Queries\Targets':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Topics':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries\Document':
case 'Utopia\Database\Validator\Queries\Documents':
case \Appwrite\Utopia\Database\Validator\Queries\Base::class:
case \Appwrite\Utopia\Database\Validator\Queries\Columns::class:
case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class:
case \Appwrite\Utopia\Database\Validator\Queries\Tables::class:
case \Appwrite\Utopia\Database\Validator\Queries\Collections::class:
case \Appwrite\Utopia\Database\Validator\Queries\Databases::class:
case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class:
case \Appwrite\Utopia\Database\Validator\Queries\Executions::class:
case \Appwrite\Utopia\Database\Validator\Queries\Files::class:
case \Appwrite\Utopia\Database\Validator\Queries\Functions::class:
case \Appwrite\Utopia\Database\Validator\Queries\Identities::class:
case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Installations::class:
case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class:
case \Appwrite\Utopia\Database\Validator\Queries\Messages::class:
case \Appwrite\Utopia\Database\Validator\Queries\Migrations::class:
case \Appwrite\Utopia\Database\Validator\Queries\Projects::class:
case \Appwrite\Utopia\Database\Validator\Queries\Providers::class:
case \Appwrite\Utopia\Database\Validator\Queries\Rules::class:
case \Appwrite\Utopia\Database\Validator\Queries\Subscribers::class:
case \Appwrite\Utopia\Database\Validator\Queries\Targets::class:
case \Appwrite\Utopia\Database\Validator\Queries\Teams::class:
case \Appwrite\Utopia\Database\Validator\Queries\Topics::class:
case \Appwrite\Utopia\Database\Validator\Queries\Users::class:
case \Appwrite\Utopia\Database\Validator\Queries\Variables::class:
case \Utopia\Database\Validator\Queries::class:
case \Utopia\Database\Validator\Queries\Document::class:
case \Utopia\Database\Validator\Queries\Documents::class:
$node['schema']['type'] = 'array';
$node['schema']['items'] = [
'type' => 'string',
];
break;
case 'Utopia\Database\Validator\Permissions':
case \Utopia\Database\Validator\Permissions::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['items'] = [
'type' => 'string',
];
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]';
break;
case 'Utopia\Database\Validator\Roles':
case \Utopia\Database\Validator\Roles::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['items'] = [
'type' => 'string',
];
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]';
break;
case 'Appwrite\Auth\Validator\Password':
case \Appwrite\Auth\Validator\Password::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'password';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'password';
break;
case 'Appwrite\Auth\Validator\Phone':
case \Appwrite\Auth\Validator\Phone::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'phone';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '+12065550100'; // In the US, 555 is reserved like example.com
break;
case 'Utopia\Validator\Range':
case \Utopia\Validator\Range::class:
/** @var Range $validator */
$node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
$node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: $validator->getMin();
break;
case 'Utopia\Validator\Integer':
case \Utopia\Validator\Integer::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = $validator->getFormat();
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\FloatValidator':
case \Utopia\Validator\Numeric::class:
case \Utopia\Validator\FloatValidator::class:
$node['schema']['type'] = 'number';
$node['schema']['format'] = 'float';
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\Length':
case \Utopia\Validator\Length::class:
$node['schema']['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\WhiteList':
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -687,11 +687,11 @@ class OpenAPI3 extends Format
}
}
break;
case 'Appwrite\Utopia\Database\Validator\CompoundUID':
case \Appwrite\Utopia\Database\Validator\CompoundUID::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<ID1:ID2>';
break;
case 'Appwrite\Utopia\Database\Validator\Operation':
case \Appwrite\Utopia\Database\Validator\Operation::class:
if ($array) {
$validator = $validator->getValidator();
}
@@ -397,51 +397,51 @@ class Swagger2 extends Format
: '';
switch ($base) {
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case \Appwrite\Utopia\Database\Validator\Queries\Base::class:
$class = $base;
break;
}
if ($class === 'Utopia\Validator\AnyOf') {
if ($class === \Utopia\Validator\AnyOf::class) {
$validator = $param['validator']->getValidators()[0];
$class = \get_class($validator);
}
$array = false;
if ($class === 'Utopia\Validator\ArrayList') {
if ($class === \Utopia\Validator\ArrayList::class) {
$array = true;
$subclass = \get_class($validator->getValidator());
switch ($subclass) {
case 'Appwrite\Utopia\Database\Validator\Operation':
case 'Utopia\Validator\WhiteList':
case \Appwrite\Utopia\Database\Validator\Operation::class:
case \Utopia\Validator\WhiteList::class:
$class = $subclass;
break;
}
}
switch ($class) {
case 'Utopia\Validator\Text':
case 'Utopia\Database\Validator\UID':
case \Utopia\Validator\Text::class:
case \Utopia\Database\Validator\UID::class:
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Validator\Boolean':
case \Utopia\Validator\Boolean::class:
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: false;
break;
case 'Appwrite\Utopia\Database\Validator\CustomId':
case \Appwrite\Utopia\Database\Validator\CustomId::class:
if ($sdk->getType() === MethodType::UPLOAD) {
$node['x-upload-id'] = true;
}
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case 'Utopia\Database\Validator\DatetimeValidator':
case \Utopia\Database\Validator\DatetimeValidator::class:
$node['type'] = $validator->getType();
$node['format'] = 'datetime';
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
break;
case 'Utopia\Database\Validator\Spatial':
case \Utopia\Database\Validator\Spatial::class:
/** @var Spatial $validator */
$node['type'] = 'array';
$node['schema']['items'] = [
@@ -455,19 +455,19 @@ class Swagger2 extends Format
Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]',
};
break;
case 'Appwrite\Network\Validator\Email':
case \Appwrite\Network\Validator\Email::class:
$node['type'] = $validator->getType();
$node['format'] = 'email';
$node['x-example'] = ($param['example'] ?? '') ?: 'email@example.com';
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Appwrite\Network\Validator\Redirect':
case \Utopia\Validator\Host::class:
case \Utopia\Validator\URL::class:
case \Appwrite\Network\Validator\Redirect::class:
$node['type'] = $validator->getType();
$node['format'] = 'url';
$node['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
break;
case 'Utopia\Validator\ArrayList':
case \Utopia\Validator\ArrayList::class:
/** @var ArrayList $validator */
$node['type'] = 'array';
$node['collectionFormat'] = 'multi';
@@ -478,34 +478,34 @@ class Swagger2 extends Format
$node['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = ($param['example'] ?? '') ?: '{}';
break;
case 'Utopia\Storage\Validator\File':
case \Utopia\Storage\Validator\File::class:
$consumes = ['multipart/form-data'];
$node['type'] = 'file';
break;
case 'Appwrite\Functions\Validator\Payload':
case \Appwrite\Functions\Validator\Payload::class:
$consumes = ['multipart/form-data'];
$node['type'] = 'payload';
break;
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries\Document':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Columns':
case 'Appwrite\Utopia\Database\Validator\Queries\Tables':
case \Appwrite\Utopia\Database\Validator\Queries\Base::class:
case \Utopia\Database\Validator\Queries::class:
case \Utopia\Database\Validator\Queries\Document::class:
case \Utopia\Database\Validator\Queries\Documents::class:
case \Appwrite\Utopia\Database\Validator\Queries\Columns::class:
case \Appwrite\Utopia\Database\Validator\Queries\Tables::class:
$node['type'] = 'array';
$node['collectionFormat'] = 'multi';
$node['items'] = [
'type' => 'string',
];
break;
case 'Utopia\Database\Validator\Permissions':
case \Utopia\Database\Validator\Permissions::class:
$node['type'] = $validator->getType();
$node['collectionFormat'] = 'multi';
$node['items'] = [
@@ -513,7 +513,7 @@ class Swagger2 extends Format
];
$node['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]';
break;
case 'Utopia\Database\Validator\Roles':
case \Utopia\Database\Validator\Roles::class:
$node['type'] = $validator->getType();
$node['collectionFormat'] = 'multi';
$node['items'] = [
@@ -521,44 +521,44 @@ class Swagger2 extends Format
];
$node['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]';
break;
case 'Appwrite\Auth\Validator\Password':
case \Appwrite\Auth\Validator\Password::class:
$node['type'] = $validator->getType();
$node['format'] = 'password';
$node['x-example'] = ($param['example'] ?? '') ?: 'password';
break;
case 'Appwrite\Auth\Validator\Phone':
case \Appwrite\Auth\Validator\Phone::class:
$node['type'] = $validator->getType();
$node['format'] = 'phone';
$node['x-example'] = ($param['example'] ?? '') ?: '+12065550100';
break;
case 'Utopia\Validator\Range':
case \Utopia\Validator\Range::class:
/** @var Range $validator */
$node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
$node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
$node['x-example'] = ($param['example'] ?? '') ?: $validator->getMin();
break;
case 'Utopia\Validator\Integer':
case \Utopia\Validator\Integer::class:
$node['type'] = $validator->getType();
$node['format'] = $validator->getFormat();
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\FloatValidator':
case \Utopia\Validator\Numeric::class:
case \Utopia\Validator\FloatValidator::class:
$node['type'] = 'number';
$node['format'] = 'float';
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\Length':
case \Utopia\Validator\Length::class:
$node['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case 'Utopia\Validator\WhiteList':
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -665,11 +665,11 @@ class Swagger2 extends Format
}
}
break;
case 'Appwrite\Utopia\Database\Validator\CompoundUID':
case \Appwrite\Utopia\Database\Validator\CompoundUID::class:
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<ID1:ID2>';
break;
case 'Appwrite\Utopia\Database\Validator\Operation':
case \Appwrite\Utopia\Database\Validator\Operation::class:
if ($array) {
$validator = $validator->getValidator();
}
+1 -1
View File
@@ -10,7 +10,7 @@ class Key extends Model
/**
* @var bool
*/
protected bool $public = false;
protected bool $public = true; // Public because reused for more key types
public function __construct()
{
@@ -7626,6 +7626,267 @@ trait DatabasesBase
$this->assertEquals(200, $update['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testInvalidRelationshipDocumentId(array $data): void
{
$databaseId = $data['databaseId'];
// Create parent table
$parentTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'ParentTable',
]);
$this->assertEquals(201, $parentTable['headers']['status-code']);
$parentTableId = $parentTable['body']['$id'];
// Create child table
$childTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'ChildTable',
]);
$this->assertEquals(201, $childTable['headers']['status-code']);
$childTableId = $childTable['body']['$id'];
// Add string column to parent
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'name',
'size' => 255,
'required' => false,
]);
// Add string column to child
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $childTableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'title',
'size' => 255,
'required' => false,
]);
// Create one-to-many relationship
$relationship = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $childTableId,
'type' => Database::RELATION_ONE_TO_MANY,
'twoWay' => false,
'key' => 'children',
]);
$this->assertEquals(202, $relationship['headers']['status-code']);
// Wait for relationship column to be available
$this->assertEventually(function () use ($databaseId, $parentTableId) {
$columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$columnKeys = array_column($columns['body']['columns'], 'key');
$this->assertContains('children', $columnKeys, "Relationship column 'children' not found in table {$parentTableId} of database {$databaseId}");
}, 2000, 200);
// ID too long (>36 chars) should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 1',
'children' => [
[
'$id' => 'this_id_is_way_too_long_and_should_fail_validation_check',
'title' => 'Child 1',
],
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// ID with invalid characters should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 2',
'children' => [
[
'$id' => 'invalid@id#with$special%chars',
'title' => 'Child 2',
],
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// ID starting with underscore should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 3',
'children' => [
[
'$id' => '_startsWithUnderscore',
'title' => 'Child 3',
],
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Valid ID should succeed
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 4',
'children' => [
[
'$id' => 'valid-id-123',
'title' => 'Child 4',
],
],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$parentRowId = $response['body']['$id'];
// Update with invalid relationship ID should fail
$response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows/' . $parentRowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'children' => [
[
'$id' => 'another@invalid#id',
'title' => 'Child 5',
],
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Invalid string relation ID should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 6',
'children' => [
'invalid@string#id',
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Integer as relation value should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 7',
'children' => [
12345,
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// unique() as $id should succeed
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 8',
'children' => [
[
'$id' => 'unique()',
'title' => 'Child 8',
],
],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Empty string as $id should fail
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 9',
'children' => [
[
'$id' => '',
'title' => 'Child 9',
],
],
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Valid ID with allowed special chars (hyphen, period) should succeed
$response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::unique(),
'data' => [
'name' => 'Parent 10',
'children' => [
[
'$id' => 'valid.id-with_chars',
'title' => 'Child 10',
],
],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
@@ -1765,12 +1765,13 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
/**
* @depends testUpdateProjectAuthLimit
*/
public function testUpdateProjectAuthSessionsLimit($data): array
public function testUpdateProjectAuthSessionsLimit(): void
{
$id = $data['projectId'] ?? '';
$id = $this->setupProject([
'projectId' => ID::unique(),
'name' => 'testUpdateProjectAuthSessionsLimit',
'region' => System::getEnv('_APP_REGION', 'default')
]);
/**
* Test for failure
@@ -1881,10 +1882,9 @@ class ProjectsConsoleClientTest extends Scope
'limit' => 10,
]);
return $data;
$this->assertEquals(200, $response['headers']['status-code']);
}
/**
* @depends testUpdateProjectAuthLimit
*/
+317 -5
View File
@@ -14,6 +14,7 @@ class KeyTest extends TestCase
{
public function testDecode(): void
{
// Decode dynamic key
$projectId = 'test';
$usage = false;
$scopes = [
@@ -22,34 +23,345 @@ class KeyTest extends TestCase
'documents.read',
];
$roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes'];
$guestRoleScopes = Config::getParam('roles', [])[User::ROLE_GUESTS]['scopes'];
$key = static::generateKey($projectId, $usage, $scopes);
$project = new Document(['$id' => $projectId,]);
$decoded = Key::decode($project, $key);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(),
key: $key,
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
$this->assertEquals('Dynamic Key', $decoded->getName());
// Decode dynamic key with extras
$extra = [
'disabledMetrics' => ['metric123'],
'hostnameOverride' => true,
'bannerDisabled' => true,
'projectCheckDisabled' => true,
'previewAuthDisabled' => true,
'deploymentStatusIgnored' => true,
];
$key = static::generateKey($projectId, $usage, $scopes, extra: $extra);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(),
key: $key,
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
$this->assertEquals('Dynamic Key', $decoded->getName());
$this->assertEquals(['metric123'], $decoded->getDisabledMetrics());
$this->assertEquals(true, $decoded->getHostnameOverride());
$this->assertEquals(true, $decoded->isBannerDisabled());
$this->assertEquals(true, $decoded->isProjectCheckDisabled());
$this->assertEquals(true, $decoded->isPreviewAuthDisabled());
$this->assertEquals(true, $decoded->isDeploymentStatusIgnored());
// Decode invalid dynamic key
$invalidKey = API_KEY_DYNAMIC . '_invalid_jwt_token';
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(),
key: $invalidKey,
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode expired dynamic key
$expiredKey = static::generateKey($projectId, $usage, $scopes, maxAge: 1, timestamp: time() - 60);
\sleep(2);
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(),
key: $expiredKey,
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode standard key
$scopes = ['custom.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId, 'keys' => [
new Document([
'secret' => 'standard_abcd1234',
'expire' => null,
'name' => 'Standard key',
'scopes' => $scopes
])
]]),
team: new Document(),
user: new Document(),
key: 'standard_abcd1234',
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
$this->assertEquals('Standard key', $decoded->getName());
// Decode deprecated standard key
$scopes = ['custom.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId, 'keys' => [
new Document([
'secret' => 'abcd1234',
'expire' => null,
'name' => 'Standard key',
'scopes' => ['custom.write']
])
]]),
team: new Document(),
user: new Document(),
key: 'abcd1234',
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
$this->assertEquals('Standard key', $decoded->getName());
// Decode invalid standard key
$scopes = ['custom.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId, 'keys' => [
new Document([
'secret' => 'standard_abcd1234',
'expire' => null,
'name' => 'Standard key',
'scopes' => ['custom.write']
])
]]),
team: new Document(),
user: new Document(),
key: 'standard_efgh5678',
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode expired standard key
$scopes = ['custom.write'];
$yesterday = (new \DateTimeImmutable('-1 day'))->format('Y-m-d\TH:i:s\Z');
$decoded = Key::decode(
project: new Document(['$id' => $projectId, 'keys' => [
new Document([
'secret' => 'standard_abcd1234',
'expire' => $yesterday,
'name' => 'Standard key',
'scopes' => $scopes
])
]]),
team: new Document(),
user: new Document(),
key: 'standard_abcd1234',
);
$this->assertEquals(true, $decoded->isExpired());
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
$this->assertEquals('Standard key', $decoded->getName());
// Decode account key
$userId = 'user123';
$scopes = ['teams.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(['$id' => $userId, 'keys' => [
new Document([
'secret' => 'account_abcd1234',
'expire' => null,
'name' => 'Account key',
'scopes' => $scopes
])
]]),
key: 'account_abcd1234',
);
$this->assertEquals('', $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals($userId, $decoded->getUserId());
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
$this->assertEquals(User::ROLE_USERS, $decoded->getRole());
$this->assertEquals($scopes, $decoded->getScopes());
$this->assertEquals('Account key', $decoded->getName());
// Decode invalid account key
$scopes = ['teams.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(['$id' => $userId, 'keys' => [
new Document([
'secret' => 'account_abcd1234',
'expire' => null,
'name' => 'Account key',
'scopes' => $scopes
])
]]),
key: 'account_efgh5678',
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode expired account key
$scopes = ['teams.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(),
user: new Document(['$id' => $userId, 'keys' => [
new Document([
'secret' => 'account_abcd1234',
'expire' => $yesterday,
'name' => 'Account key',
'scopes' => $scopes
])
]]),
key: 'account_abcd1234',
);
$this->assertEquals(true, $decoded->isExpired());
$this->assertEquals('', $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals($userId, $decoded->getUserId());
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
$this->assertEquals(User::ROLE_USERS, $decoded->getRole());
$this->assertEquals($scopes, $decoded->getScopes());
$this->assertEquals('Account key', $decoded->getName());
// Decode organization key
$teamId = 'team123';
$scopes = ['projects.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(['$id' => $teamId, 'keys' => [
new Document([
'secret' => 'organization_abcd1234',
'expire' => null,
'name' => 'Organization key',
'scopes' => $scopes
])
]]),
user: new Document(),
key: 'organization_abcd1234',
);
$this->assertEquals('', $decoded->getProjectId());
$this->assertEquals($teamId, $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals($scopes, $decoded->getScopes());
$this->assertEquals('Organization key', $decoded->getName());
// Decode invalid organization key
$scopes = ['projects.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(['$id' => $teamId, 'keys' => [
new Document([
'secret' => 'organization_abcd1234',
'expire' => null,
'name' => 'Organization key',
'scopes' => $scopes
])
]]),
user: new Document(),
key: 'organization_efgh5678',
);
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals('', $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
$this->assertEquals('UNKNOWN', $decoded->getName());
// Decode expired organization key
$scopes = ['projects.write'];
$decoded = Key::decode(
project: new Document(['$id' => $projectId]),
team: new Document(['$id' => $teamId, 'keys' => [
new Document([
'secret' => 'organization_abcd1234',
'expire' => $yesterday,
'name' => 'Organization key',
'scopes' => $scopes
])
]]),
user: new Document(),
key: 'organization_abcd1234',
);
$this->assertEquals(true, $decoded->isExpired());
$this->assertEquals('', $decoded->getProjectId());
$this->assertEquals($teamId, $decoded->getTeamId());
$this->assertEquals('', $decoded->getUserId());
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
$this->assertEquals($scopes, $decoded->getScopes());
$this->assertEquals('Organization key', $decoded->getName());
}
private static function generateKey(
string $projectId,
bool $usage,
array $scopes,
int $maxAge = 86400,
?int $timestamp = null,
array $extra = []
): string {
$jwt = new JWT(
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
algo: 'HS256',
maxAge: 86400,
maxAge: $maxAge,
leeway: 0,
);
$jwt->setTestTimestamp($timestamp);
$apiKey = $jwt->encode([
$apiKey = $jwt->encode(\array_merge([
'projectId' => $projectId,
'usage' => $usage,
'scopes' => $scopes,
]);
], $extra));
return API_KEY_DYNAMIC . '_' . $apiKey;
}