mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/add-smtp-migration' into add-custom-domains-migration
# Conflicts: # composer.lock
This commit is contained in:
+1
-1
@@ -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
@@ -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 +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 +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 +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 +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 +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 +0,0 @@
|
||||
Update a project by its unique ID.
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user