Merge remote-tracking branch 'origin/add-smtp-migration' into add-custom-domains-migration

# Conflicts:
#	composer.lock
This commit is contained in:
Prem Palanisamy
2026-05-21 14:45:01 +01:00
33 changed files with 1289 additions and 83 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIS
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
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('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, true));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
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));
Generated
+4 -4
View File
@@ -4679,12 +4679,12 @@
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "7af2ef5b766d3eb34fa8f83f2c43c84c853f499a"
"reference": "cc7d68aaadf2b37adcce7735b9e2f068cc78caf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7af2ef5b766d3eb34fa8f83f2c43c84c853f499a",
"reference": "7af2ef5b766d3eb34fa8f83f2c43c84c853f499a",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/cc7d68aaadf2b37adcce7735b9e2f068cc78caf9",
"reference": "cc7d68aaadf2b37adcce7735b9e2f068cc78caf9",
"shasum": ""
},
"require": {
@@ -4744,7 +4744,7 @@
"source": "https://github.com/utopia-php/migration/tree/add-custom-domains-migration",
"issues": "https://github.com/utopia-php/migration/issues"
},
"time": "2026-05-21T10:22:19+00:00"
"time": "2026-05-21T13:40:46+00:00"
},
{
"name": "utopia-php/mongo",
-1
View File
@@ -1 +0,0 @@
Create a new JWT token. This token can be used to authenticate users with custom scopes and expiration time.
@@ -1 +0,0 @@
Send a test email to verify SMTP configuration.
-1
View File
@@ -1 +0,0 @@
Create a new project. You can create a maximum of 100 projects per account.
@@ -1 +0,0 @@
Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state.
-1
View File
@@ -1 +0,0 @@
Delete a project by its unique ID.
@@ -1 +0,0 @@
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
-1
View File
@@ -1 +0,0 @@
Get a project by its unique ID. This endpoint allows you to retrieve the project's details, including its name, description, team, region, and other metadata.
@@ -1 +0,0 @@
Update the status of a specific authentication method. Use this endpoint to enable or disable different authentication methods such as email, magic urls or sms in your project.
@@ -1 +0,0 @@
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
@@ -1 +0,0 @@
Update the list of mock phone numbers for testing. Use these numbers to bypass SMS verification in development.
@@ -1 +0,0 @@
Update the OAuth2 provider configurations. Use this endpoint to set up or update the OAuth2 provider credentials or enable/disable providers.
-1
View File
@@ -1 +0,0 @@
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
-1
View File
@@ -1 +0,0 @@
Update a project by its unique ID.
+2
View File
@@ -11,6 +11,7 @@ use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Migrations;
use Appwrite\Platform\Modules\Organization;
use Appwrite\Platform\Modules\Presences;
use Appwrite\Platform\Modules\Project;
use Appwrite\Platform\Modules\Projects;
@@ -44,6 +45,7 @@ class Appwrite extends Platform
$this->addModule(new VCS\Module());
$this->addModule(new Webhooks\Module());
$this->addModule(new Migrations\Module());
$this->addModule(new Organization\Module());
$this->addModule(new Project\Module());
$this->addModule(new Advisor\Module());
}
@@ -0,0 +1,28 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Platform\Action;
class Init extends Action
{
public static function getName(): string
{
return 'init';
}
public function __construct()
{
$this
->setType(Action::TYPE_INIT)
->groups(['organization'])
->inject('team')
->callback(function (Document $team) {
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
}
});
}
}
@@ -0,0 +1,11 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Platform\Action as AppwriteAction;
use Appwrite\Platform\Permission as AppwritePermission;
class Action extends AppwriteAction
{
use AppwritePermission;
}
@@ -0,0 +1,233 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\ProjectId;
use Appwrite\Utopia\Response;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
use Utopia\Platform\Scope\HTTP;
use Utopia\Pools\Group;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createOrganizationProject';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/organization/projects')
->desc('Create organization project')
->groups(['api', 'organization'])
->label('audits.event', 'projects.create')
->label('audits.resource', 'project/{response.$id}')
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'organization',
group: 'projects',
name: 'createProject',
description: <<<EOT
Create a new project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROJECT,
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true)
->inject('response')
->inject('dbForPlatform')
->inject('cache')
->inject('pools')
->inject('hooks')
->inject('team')
->callback($this->action(...));
}
public function action(string $projectId, string $name, string $region, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team)
{
$allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', '')));
if (!empty($allowList) && !\in_array($region, $allowList)) {
throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported');
}
$auth = Config::getParam('auth', []);
$auths = [
'limit' => 0,
'maxSessions' => 0,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'disposableEmails' => false,
'canonicalEmails' => false,
'freeEmails' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
'membershipsUserName' => false,
'membershipsUserEmail' => false,
'membershipsMfa' => false,
'membershipsUserId' => false,
'membershipsUserPhone' => false,
'invalidateSessions' => true
];
foreach ($auth as $method) {
$auths[$method['key'] ?? ''] = true;
}
$projectId = ($projectId == 'unique()') ? ID::unique() : $projectId;
if ($projectId === 'console') {
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
}
$databases = Config::getParam('pools-database', []);
if ($region !== 'default') {
$databaseKeys = System::getEnv('_APP_DATABASE_KEYS', '');
$keys = explode(',', $databaseKeys);
$databases = array_filter($keys, function ($value) use ($region) {
return str_contains($value, $region);
});
}
$databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE');
$index = \array_search($databaseOverride, $databases);
if ($index !== false) {
$dsn = $databases[$index];
} else {
$dsn = $databases[array_rand($databases)];
}
// TODO: Temporary until all projects are using shared tables.
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn, $sharedTables)) {
$schema = 'appwrite';
$database = 'appwrite';
$namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', '');
$dsn = $schema . '://' . $dsn . '?database=' . $database;
if (!empty($namespace)) {
$dsn .= '&namespace=' . $namespace;
}
}
try {
$project = $dbForPlatform->createDocument('projects', new Document([
'$id' => $projectId,
'$permissions' => $this->getPermissions($team->getId(), $projectId),
'name' => $name,
'teamInternalId' => $team->getSequence(),
'teamId' => $team->getId(),
'region' => $region,
'version' => APP_VERSION_STABLE,
'services' => new \stdClass(),
'platforms' => null,
'oAuthProviders' => [],
'webhooks' => null,
'keys' => null,
'auths' => $auths,
'accessedAt' => DateTime::now(),
'search' => implode(' ', [$projectId, $name]),
'database' => $dsn,
'labels' => [],
'status' => PROJECT_STATUS_ACTIVE,
]));
} catch (Duplicate) {
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
}
try {
$dsn = new DSN($dsn);
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $dsn);
}
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
if ($projectTables) {
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$dbForProject = new Database($adapter, $cache);
$dbForProject
->setDatabase(APP_DATABASE)
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
$create = true;
try {
$dbForProject->create();
} catch (Duplicate) {
$create = false;
}
$adapter = new AdapterDatabase($dbForProject);
$audit = new Audit($adapter);
$audit->setup();
if ($create) {
/** @var array $collections */
$collections = Config::getParam('collections', [])['projects'] ?? [];
foreach ($collections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
try {
$dbForProject->createCollection($key, $attributes, $indexes);
} catch (Duplicate) {
// Collection already exists
}
}
}
}
// Hook allowing instant project mirroring during migration
// Outside of migration, hook is not registered and has no effect
$hooks->trigger('afterProjectCreation', [$project, $pools, $cache]);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteOrganizationProject';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/organization/projects/:projectId')
->desc('Delete organization project')
->groups(['api', 'organization'])
->label('scope', 'projects.write')
->label('audits.event', 'projects.delete')
->label('audits.resource', 'project/{request.projectId}')
->label('sdk', new Method(
namespace: 'organization',
group: 'projects',
name: 'deleteProject',
description: <<<EOT
Delete a project by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForPlatform')
->inject('publisherForDeletes')
->inject('authorization')
->inject('team')
->callback($this->action(...));
}
public function action(
string $projectId,
Response $response,
Database $dbForPlatform,
DeletePublisher $publisherForDeletes,
Authorization $authorization,
Document $team,
) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($project->getAttribute('teamInternalId') !== $team->getSequence()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId()))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB');
}
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $project,
));
$response->noContent();
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getOrganizationProject';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/organization/projects/:projectId')
->desc('Get organization project')
->groups(['api', 'organization'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'organization',
group: 'projects',
name: 'getProject',
description: <<<EOT
Get a project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForPlatform')
->inject('team')
->callback($this->action(...));
}
public function action(
string $projectId,
Response $response,
Database $dbForPlatform,
Document $team,
) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($project->getAttribute('teamInternalId') !== $team->getSequence()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateOrganizationProject';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/organization/projects/:projectId')
->desc('Update organization project')
->groups(['api', 'organization'])
->label('scope', 'projects.write')
->label('audits.event', 'projects.update')
->label('audits.resource', 'project/{response.$id}')
->label('sdk', new Method(
namespace: 'organization',
group: 'projects',
name: 'updateProject',
description: <<<EOT
Update a project by its unique ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->inject('response')
->inject('dbForPlatform')
->inject('team')
->callback($this->action(...));
}
public function action(string $projectId, string $name, Response $response, Database $dbForPlatform, Document $team)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($project->getAttribute('teamInternalId') !== $team->getSequence()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'name' => $name,
'search' => implode(' ', [$projectId, $name]),
]));
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,196 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\ListSelection;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
{
use HTTP;
// cached mapping of columns to their subQuery filters
private static ?array $attributeToSubQueryFilters = null;
public static function getName()
{
return 'listOrganizationProjects';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/organization/projects')
->desc('List organization projects')
->groups(['api', 'organization'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'organization',
group: 'projects',
name: 'listProjects',
description: <<<EOT
Get a list of all projects. You can use the query params to filter your results.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT_LIST
)
],
contentType: ContentType::JSON
))
->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->inject('team')
->callback($this->action(...));
}
public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team)
{
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
$queries[] = Query::equal('teamInternalId', [$team->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$projectId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('projects', $projectId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$selectQueries = Query::groupByType($queries)['selections'];
$filterQueries = Query::groupByType($queries)['filters'];
$projects = $this->find($dbForPlatform, $queries, $selectQueries);
$total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (Order $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->addFilter(new ListSelection($selectQueries, 'projects'));
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document([
'projects' => $projects,
'total' => $total,
]), Response::MODEL_PROJECT_LIST);
}
// Build mapping of columns to their subQuery filters
private static function getAttributeToSubQueryFilters(): array
{
if (self::$attributeToSubQueryFilters !== null) {
return self::$attributeToSubQueryFilters;
}
self::$attributeToSubQueryFilters = [];
$collections = Config::getParam('collections', []);
$projectAttributes = $collections['platform']['projects']['attributes'] ?? [];
foreach ($projectAttributes as $attribute) {
$attributeId = $attribute['$id'] ?? null;
$filters = $attribute['filters'] ?? [];
if ($attributeId === null || empty($filters)) {
continue;
}
// extract only subQuery filters
$subQueryFilters = \array_filter($filters, function ($filter) {
return \str_starts_with($filter, 'subQuery');
});
if (!empty($subQueryFilters)) {
self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters);
}
}
return self::$attributeToSubQueryFilters;
}
private function find(Database $dbForPlatform, array $queries, array $selectQueries): array
{
if (empty($selectQueries)) {
return $dbForPlatform->find('projects', $queries);
}
$selectedAttributes = [];
foreach ($selectQueries as $query) {
foreach ($query->getValues() as $value) {
$selectedAttributes[] = $value;
}
}
if (\in_array('*', $selectedAttributes)) {
return $dbForPlatform->find('projects', $queries);
}
$filtersToSkipMap = [];
$selectedAttributesMap = \array_flip($selectedAttributes);
$attributeToSubQueryFilters = self::getAttributeToSubQueryFilters();
foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) {
if (!isset($selectedAttributesMap[$attributeName])) {
foreach ($subQueryFilters as $filter) {
$filtersToSkipMap[$filter] = true;
}
}
}
$filtersToSkip = \array_keys($filtersToSkipMap);
return empty($filtersToSkip)
? $dbForPlatform->find('projects', $queries)
: $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Organization;
use Appwrite\Platform\Modules\Organization\Services\Http;
use Utopia\Platform\Module as Base;
class Module extends Base
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,29 @@
<?php
namespace Appwrite\Platform\Modules\Organization\Services;
use Appwrite\Platform\Modules\Organization\Http\Init as Init;
use Appwrite\Platform\Modules\Organization\Http\Projects\Create as CreateProject;
use Appwrite\Platform\Modules\Organization\Http\Projects\Delete as DeleteProject;
use Appwrite\Platform\Modules\Organization\Http\Projects\Get as GetProject;
use Appwrite\Platform\Modules\Organization\Http\Projects\Update as UpdateProject;
use Appwrite\Platform\Modules\Organization\Http\Projects\XList as ListProjects;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Init hook
$this->addAction(Init::getName(), new Init());
// Projects
$this->addAction(CreateProject::getName(), new CreateProject());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(GetProject::getName(), new GetProject());
$this->addAction(UpdateProject::getName(), new UpdateProject());
$this->addAction(DeleteProject::getName(), new DeleteProject());
}
}
@@ -4,9 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\ProjectId;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Request;
@@ -54,19 +51,6 @@ class Create extends Action
->label('audits.event', 'projects.create')
->label('audits.resource', 'project/{response.$id}')
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'create',
description: '/docs/references/projects/create.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('teamId', '', new UID(), 'Team unique ID.')
@@ -3,9 +3,6 @@
namespace Appwrite\Platform\Modules\Projects\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
@@ -39,19 +36,6 @@ class Update extends Action
->label('scope', 'projects.write')
->label('audits.event', 'projects.update')
->label('audits.resource', 'project/{request.projectId}')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'update',
description: '/docs/references/projects/update.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true)
@@ -4,10 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\ListSelection;
@@ -48,22 +44,6 @@ class XList extends Action
->desc('List projects')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'list',
description: <<<EOT
Get a list of all projects. You can use the query params to filter your results.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT_LIST
)
],
contentType: ContentType::JSON
))
->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
@@ -2778,17 +2778,17 @@ class FunctionsCustomServerTest extends Scope
$this->assertEmpty($executions['body']['executions'][0]['logs']);
$this->assertEmpty($executions['body']['executions'][0]['errors']);
// Ensure executions count
$executions = $this->listExecutions($functionId);
$this->assertEventually(function () use ($functionId) {
$executions = $this->listExecutions($functionId);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(3, $executions['body']['executions']);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(3, $executions['body']['executions']);
// Double check logs and errors are empty
foreach ($executions['body']['executions'] as $execution) {
$this->assertEmpty($execution['logs']);
$this->assertEmpty($execution['errors']);
}
foreach ($executions['body']['executions'] as $execution) {
$this->assertEmpty($execution['logs']);
$this->assertEmpty($execution['errors']);
}
}, 10000, 500);
$this->cleanupFunction($functionId);
}
@@ -2554,10 +2554,14 @@ trait MigrationsBase
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
];
// Unique name so re-runs and parallel suites can't match a stale key
// left behind by a previous crashed run.
$keyName = 'Test API Key ' . ID::unique();
// Create API key on source project
$response = $this->client->call(Client::METHOD_POST, '/project/keys', $sourceHeaders, [
'keyId' => ID::unique(),
'name' => 'Test API Key',
'name' => $keyName,
'scopes' => ['databases.read', 'databases.write'],
'expire' => null,
]);
@@ -2596,7 +2600,7 @@ trait MigrationsBase
$foundKey = null;
foreach ($response['body']['keys'] as $k) {
if ($k['name'] === 'Test API Key') {
if ($k['name'] === $keyName) {
$foundKey = $k;
break;
@@ -2604,7 +2608,7 @@ trait MigrationsBase
}
$this->assertNotNull($foundKey);
$this->assertEquals('Test API Key', $foundKey['name']);
$this->assertEquals($keyName, $foundKey['name']);
$this->assertEqualsCanonicalizing(['databases.read', 'databases.write'], $foundKey['scopes']);
$this->assertEmpty($foundKey['expire']);
$this->assertNotEquals($apiKey['secret'], $foundKey['secret']);
@@ -0,0 +1,484 @@
<?php
namespace Tests\E2E\Services\Organization;
use Appwrite\Extend\Exception;
use Tests\E2E\Client;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\System\System;
trait ProjectsBase
{
private static array $cachedOrganization = [];
private static array $cachedProjectData = [];
/**
* Setup and cache an organization (team) for organization endpoint tests.
*/
protected function setupOrganization(): array
{
if (!empty(self::$cachedOrganization)) {
return self::$cachedOrganization;
}
$teamId = ID::unique();
$team = null;
for ($i = 0; $i < 3; $i++) {
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => $teamId,
'name' => 'Organization Test',
]);
if (\in_array($team['headers']['status-code'], [201, 409])) {
break;
}
\usleep(500000);
}
$this->assertContains($team['headers']['status-code'], [201, 409], 'Setup organization (team) failed');
self::$cachedOrganization = [
'teamId' => $team['body']['$id'] ?? $teamId,
];
return self::$cachedOrganization;
}
protected function getOrganizationHeaders(): array
{
$organization = $this->setupOrganization();
return array_merge($this->getHeaders(), [
'x-appwrite-organization' => $organization['teamId'],
]);
}
/**
* Setup and cache a project created via organization endpoints.
*/
protected function setupOrganizationProject(): array
{
if (!empty(self::$cachedProjectData)) {
return self::$cachedProjectData;
}
$project = null;
for ($i = 0; $i < 3; $i++) {
$project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => ID::unique(),
'name' => 'Organization Project Test',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
if ($project['headers']['status-code'] === 201) {
break;
}
\usleep(500000);
}
$this->assertEquals(201, $project['headers']['status-code'], 'Setup organization project failed');
self::$cachedProjectData = [
'projectId' => $project['body']['$id'],
'teamId' => $this->setupOrganization()['teamId'],
];
return self::$cachedProjectData;
}
public function testCreateProject(): void
{
$organization = $this->setupOrganization();
$teamId = $organization['teamId'];
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => ID::unique(),
'name' => 'Organization Project Test',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('Organization Project Test', $response['body']['name']);
$this->assertEquals($teamId, $response['body']['teamId']);
$this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']);
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
/**
* Test for FAILURE - missing organization header
*/
$response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Organization Project Test',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for FAILURE - empty name
*/
$response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => ID::unique(),
'name' => '',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testCreateDuplicateProject(): void
{
$organization = $this->setupOrganization();
$projectId = ID::unique();
$response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => $projectId,
'name' => 'Original Organization Project',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for FAILURE - duplicate project ID
*/
$response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => $projectId,
'name' => 'Duplicate Organization Project',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(409, $response['headers']['status-code']);
$this->assertEquals(409, $response['body']['code']);
$this->assertEquals(Exception::PROJECT_ALREADY_EXISTS, $response['body']['type']);
}
public function testGetProject(): void
{
$data = $this->setupOrganizationProject();
$projectId = $data['projectId'];
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($projectId, $response['body']['$id']);
$this->assertEquals('Organization Project Test', $response['body']['name']);
$this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']);
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
/**
* Test for FAILURE - project not found
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . ID::unique(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for FAILURE - project from different organization
*/
$otherTeam = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Other Organization',
]);
$this->assertContains($otherTeam['headers']['status-code'], [201, 409]);
$otherTeamId = $otherTeam['body']['$id'] ?? $otherTeam['body']['teamId'];
$otherProject = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], array_merge($this->getHeaders(), [
'x-appwrite-organization' => $otherTeamId,
])), [
'projectId' => ID::unique(),
'name' => 'Other Organization Project',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $otherProject['headers']['status-code']);
$otherProjectId = $otherProject['body']['$id'];
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $otherProjectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
}
public function testUpdateProject(): void
{
$data = $this->setupOrganizationProject();
$projectId = $data['projectId'];
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'name' => 'Updated Organization Project',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($projectId, $response['body']['$id']);
$this->assertEquals('Updated Organization Project', $response['body']['name']);
/**
* Test for FAILURE - project not found
*/
$response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . ID::unique(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'name' => 'Should Fail',
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for FAILURE - empty name
*/
$response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'name' => '',
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testDeleteProject(): void
{
$organization = $this->setupOrganization();
$project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project To Delete',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $project['headers']['status-code']);
$projectId = $project['body']['$id'];
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
// Verify project is actually deleted
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for FAILURE - project not found (already deleted)
*/
$response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
}
public function testListProjects(): void
{
$organization = $this->setupOrganization();
$teamId = $organization['teamId'];
// Create a second project in the same organization
$project2 = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'projectId' => ID::unique(),
'name' => 'Second Organization Project',
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $project2['headers']['status-code']);
$project2Id = $project2['body']['$id'];
/**
* Test for SUCCESS - basic list
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$this->assertGreaterThan(0, $response['body']['total']);
/**
* Test search queries
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders(), [
'search' => 'Second Organization Project',
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, $response['body']['total']);
$this->assertIsArray($response['body']['projects']);
$this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']);
/**
* Test pagination with limit
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::limit(1)->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['projects']);
/**
* Test pagination with offset
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::offset(1)->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
/**
* Test query by name
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::equal('name', ['Second Organization Project'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, count($response['body']['projects']));
$this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']);
/**
* Test cursor pagination
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['projects']);
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
/**
* Test for FAILURE - invalid cursor
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(),
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testListProjectsQuerySelect(): void
{
$data = $this->setupOrganizationProject();
$projectId = $data['projectId'];
$response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getOrganizationHeaders()), [
'queries' => [
Query::select(['name'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['projects']);
$this->assertEquals('Organization Project Test', $response['body']['projects'][0]['name']);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Organization;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ProjectsConsoleClientTest extends Scope
{
use ProjectsBase;
use ProjectConsole;
use SideConsole;
}
@@ -1802,7 +1802,13 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals('en-us', $response['body']['locale']);
/** Update Email template, fail due to SMTP disabled */
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
$projectWithoutSmtp = $this->setupProject([
'projectId' => ID::unique(),
'name' => 'Project Without SMTP',
'region' => System::getEnv('_APP_REGION', 'default')
]);
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectWithoutSmtp . '/templates/email/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.1',