Merge branch 'main' into 'fix-teams-deletion'.

This commit is contained in:
Darshan
2025-05-27 19:16:40 +05:30
5916 changed files with 304542 additions and 9332 deletions
+5
View File
@@ -103,6 +103,11 @@ class Auth
*/
public static $cookieName = 'a_session';
/**
* @var string
*/
public static $cookieNamePreview = 'a_jwt_console';
/**
* User Unique ID.
*
+44 -2
View File
@@ -20,6 +20,11 @@ class Key
protected string $name,
protected bool $expired = false,
protected array $disabledMetrics = [],
protected bool $hostnameOverride = false,
protected bool $bannerDisabled = false,
protected bool $projectCheckDisabled = false,
protected bool $previewAuthDisabled = false,
protected bool $deploymentStatusIgnored = false,
) {
}
@@ -58,6 +63,33 @@ class Key
return $this->disabledMetrics;
}
public function getHostnameOverride(): bool
{
return $this->hostnameOverride;
}
public function isBannerDisabled(): bool
{
return $this->bannerDisabled;
}
public function isPreviewAuthDisabled(): bool
{
return $this->previewAuthDisabled;
}
public function isDeploymentStatusIgnored(): bool
{
return $this->deploymentStatusIgnored;
}
public function isProjectCheckDisabled(): bool
{
return $this->projectCheckDisabled;
}
/**
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
* Can be a stored API key or a dynamic key (JWT).
@@ -109,9 +141,14 @@ class Key
$name = $payload['name'] ?? 'Dynamic Key';
$projectId = $payload['projectId'] ?? '';
$disabledMetrics = $payload['disabledMetrics'] ?? [];
$hostnameOverride = $payload['hostnameOverride'] ?? false;
$bannerDisabled = $payload['bannerDisabled'] ?? false;
$projectCheckDisabled = $payload['projectCheckDisabled'] ?? false;
$previewAuthDisabled = $payload['previewAuthDisabled'] ?? false;
$deploymentStatusIgnored = $payload['deploymentStatusIgnored'] ?? false;
$scopes = \array_merge($payload['scopes'] ?? [], $scopes);
if ($projectId !== $project->getId()) {
if (!$projectCheckDisabled && $projectId !== $project->getId()) {
return $guestKey;
}
@@ -122,7 +159,12 @@ class Key
$scopes,
$name,
$expired,
$disabledMetrics
$disabledMetrics,
$hostnameOverride,
$bannerDisabled,
$projectCheckDisabled,
$previewAuthDisabled,
$deploymentStatusIgnored
);
case API_KEY_STANDARD:
$key = $project->find(
+2 -2
View File
@@ -6,9 +6,9 @@ use Utopia\Logger\Log;
interface Adapter
{
public function issueCertificate(string $certName, string $domain): ?string;
public function issueCertificate(string $certName, string $domain, ?string $domainType): ?string;
public function isRenewRequired(string $domain, Log $log): bool;
public function isRenewRequired(string $domain, ?string $domainType, Log $log): bool;
public function deleteCertificate(string $domain): void;
}
+2 -2
View File
@@ -18,7 +18,7 @@ class LetsEncrypt implements Adapter
}
public function issueCertificate(string $certName, string $domain): ?string
public function issueCertificate(string $certName, string $domain, ?string $domainType): ?string
{
$stdout = '';
$stderr = '';
@@ -84,7 +84,7 @@ class LetsEncrypt implements Adapter
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30);
}
public function isRenewRequired(string $domain, Log $log): bool
public function isRenewRequired(string $domain, ?string $domainType, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
+1 -1
View File
@@ -42,7 +42,7 @@ class Realtime extends Event
* Set subscribers for this realtime event.
*
* @param array $subscribers
* @return array
* @return self
*/
public function setSubscribers(array $subscribers): self
{
+42 -1
View File
@@ -118,6 +118,9 @@ class Exception extends \Exception
public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
public const TEAM_ALREADY_EXISTS = 'team_already_exists';
/** Console */
public const RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
/** Membership */
public const MEMBERSHIP_NOT_FOUND = 'membership_not_found';
public const MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed';
@@ -151,12 +154,18 @@ class Exception extends \Exception
public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict';
public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
/** Sites */
public const SITE_NOT_FOUND = 'site_not_found';
public const SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
/** Functions */
public const FUNCTION_NOT_FOUND = 'function_not_found';
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
public const FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
@@ -166,11 +175,16 @@ class Exception extends \Exception
public const BUILD_NOT_READY = 'build_not_ready';
public const BUILD_IN_PROGRESS = 'build_in_progress';
public const BUILD_ALREADY_COMPLETED = 'build_already_completed';
public const BUILD_CANCELED = 'build_canceled';
public const BUILD_FAILED = 'build_failed';
/** Execution */
public const EXECUTION_NOT_FOUND = 'execution_not_found';
public const EXECUTION_IN_PROGRESS = 'execution_in_progress';
/** Log */
public const LOG_NOT_FOUND = 'log_not_found';
/** Databases */
public const DATABASE_NOT_FOUND = 'database_not_found';
public const DATABASE_ALREADY_EXISTS = 'database_already_exists';
@@ -248,6 +262,7 @@ class Exception extends \Exception
/** Variables */
public const VARIABLE_NOT_FOUND = 'variable_not_found';
public const VARIABLE_ALREADY_EXISTS = 'variable_already_exists';
public const VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret';
/** Platform */
public const PLATFORM_NOT_FOUND = 'platform_not_found';
@@ -304,15 +319,22 @@ class Exception extends \Exception
/** Schedules */
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
/** Tokens */
public const TOKEN_NOT_FOUND = 'token_not_found';
public const TOKEN_EXPIRED = 'token_expired';
public const TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
protected string $type = '';
protected array $errors = [];
protected bool $publish;
private array $ctas = [];
private ?string $view = null;
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null)
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, ?string $view = null)
{
$this->errors = Config::getParam('errors');
$this->type = $type;
$this->view = $view;
$this->code = $code ?? $this->errors[$type]['code'];
// Mark string errors like HY001 from PDO as 500 errors
@@ -362,4 +384,23 @@ class Exception extends \Exception
{
return $this->publish;
}
public function addCTA(string $label, ?string $url = null): self
{
$this->ctas[] = [
'label' => $label,
'url' => $url
];
return $this;
}
public function getCTAs(): array
{
return $this->ctas;
}
public function getView(): ?string
{
return $this->view;
}
}
+14 -1
View File
@@ -272,7 +272,6 @@ class Realtime extends MessagingAdapter
$channels[] = 'account.' . $parts[1];
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'migrations':
case 'rules':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
@@ -353,6 +352,20 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'sites':
if ($parts[2] === 'deployments') {
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'migrations':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
}
+171 -157
View File
@@ -3,48 +3,37 @@
namespace Appwrite\Migration;
use Exception;
use Swoole\Runtime;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
abstract class Migration
{
/**
* @var int
*/
protected int $limit = 100;
/**
* @var Document
*/
protected Document $project;
/**
* @var Database
*/
protected Database $projectDB;
protected Database $dbForProject;
protected Database $dbForPlatform;
/**
* @var Database
* @var callable(Document): Database
*/
protected Database $consoleDB;
protected mixed $getProjectDB;
/**
* @var PDO
*/
protected PDO $pdo;
/**
* @var array
* @var array<string, string>
*/
public static array $versions = [
'1.0.0-RC1' => 'V15',
@@ -94,10 +83,15 @@ abstract class Migration
'1.6.0' => 'V21',
'1.6.1' => 'V21',
'1.6.2' => 'V21',
'1.7.0-RC1' => 'V22',
'1.7.0' => 'V22',
'1.7.1' => 'V22',
'1.7.2' => 'V22',
'1.7.3' => 'V22',
];
/**
* @var array
* @var array<string, array<string, mixed>>
*/
protected array $collections;
@@ -108,38 +102,35 @@ abstract class Migration
$this->collections = Config::getParam('collections', []);
$projectCollections = $this->collections['projects'];
$this->collections['projects']['_metadata'] = [
'$id' => ID::custom('_metadata'),
'$collection' => Database::METADATA,
];
$this->collections['projects'] = array_merge([
'_metadata' => [
'$id' => ID::custom('_metadata'),
'$collection' => Database::METADATA
],
'audit' => [
'$id' => ID::custom('audit'),
'$collection' => Database::METADATA
],
'abuse' => [
'$id' => ID::custom('abuse'),
'$collection' => Database::METADATA
]
], $projectCollections);
$this->collections['projects']['audit'] = [
'$id' => ID::custom('audit'),
'$collection' => Database::METADATA,
];
}
/**
* Set project for migration.
*
* @param Document $project
* @param Database $projectDB
* @param Database $oldConsoleDB
*
* @param Database $dbForProject
* @param Database $dbForPlatform
* @return self
*/
public function setProject(Document $project, Database $projectDB, Database $consoleDB): self
{
public function setProject(
Document $project,
Database $dbForProject,
Database $dbForPlatform,
?callable $getProjectDB = null
): self {
$this->project = $project;
$this->projectDB = $projectDB;
$this->consoleDB = $consoleDB;
$this->dbForProject = $dbForProject;
$this->dbForPlatform = $dbForPlatform;
$this->getProjectDB = $getProjectDB;
return $this;
}
@@ -148,7 +139,7 @@ abstract class Migration
* Set PDO for Migration.
*
* @param PDO $pdo
* @return \Appwrite\Migration\Migration
* @return Migration
*/
public function setPDO(PDO $pdo): self
{
@@ -161,87 +152,49 @@ abstract class Migration
* Iterates through every document.
*
* @param callable $callback
* @throws Exception
*/
public function forEachDocument(callable $callback): void
{
$internalProjectId = $this->project->getInternalId();
$projectInternalId = $this->project->getInternalId();
$collections = match ($internalProjectId) {
$collections = match ($projectInternalId) {
'console' => $this->collections['console'],
default => $this->collections['projects'],
};
foreach ($collections as $collection) {
// Only migrate top-level collections
if ($collection['$collection'] !== Database::METADATA) {
continue;
}
Console::log('Migrating Collection ' . $collection['$id'] . ':');
Console::log('Migrating documents for collection "' . $collection['$id'] . '"');
foreach ($this->documentsIterator($collection['$id']) as $document) {
go(function (Document $document, callable $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (is_null($new) || $new->getArrayCopy() == $old) {
return;
}
try {
$this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
}
}, $document, $callback);
}
}
}
/**
* Provides an iterator for all documents on a collection.
*
* @param string $collectionId
* @return iterable<Document>
* @throws \Exception
*/
public function documentsIterator(string $collectionId, $queries = []): iterable
{
$sum = 0;
$nextDocument = null;
$collectionCount = $this->projectDB->count($collectionId);
$queries[] = Query::limit($this->limit);
do {
if ($nextDocument !== null) {
$cursorQueryIndex = \array_search('cursorAfter', \array_map(fn (Query $query) => $query->getMethod(), $queries));
if ($cursorQueryIndex !== false) {
$queries[$cursorQueryIndex] = Query::cursorAfter($nextDocument);
} else {
$queries[] = Query::cursorAfter($nextDocument);
$this->dbForProject->foreach($collection['$id'], function (Document $document) use ($collection, $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
}
$documents = $this->projectDB->find($collectionId, $queries);
$count = count($documents);
$sum += $count;
$old = $document->getArrayCopy();
$new = $callback($document);
Console::log($sum . ' / ' . $collectionCount);
foreach ($documents as $document) {
yield $document;
}
if ($new === null || $new->getArrayCopy() == $old) {
return;
}
if ($count !== $this->limit) {
$nextDocument = null;
} else {
$nextDocument = end($documents);
}
} while (!is_null($nextDocument));
try {
$this->dbForProject->updateDocument(
$document->getCollection(),
$document->getId(),
$document
);
} catch (\Throwable $th) {
Console::error("Failed to update document \"{$document->getId()}\" in collection \"{$collection['$id']}\":" . $th->getMessage());
return;
}
});
}
}
/**
@@ -261,55 +214,50 @@ abstract class Migration
default => 'projects',
};
if (!$this->projectDB->exists(System::getEnv('_APP_DB_SCHEMA', 'appwrite'), $name)) {
$attributes = [];
$indexes = [];
$collection = $this->collections[$collectionType][$id];
if (!$this->dbForProject->getCollection($id)->isEmpty()) {
return;
}
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'default' => $attribute['default'] ?? null,
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
]);
}
$collection = $this->collections[$collectionType][$id];
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$attributes = [];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document($attribute);
}
try {
$this->projectDB->createCollection($name, $attributes, $indexes);
} catch (\Throwable $th) {
throw $th;
}
$indexes = [];
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document($index);
}
try {
$this->dbForProject->createCollection($name, $attributes, $indexes);
} catch (Duplicate) {
Console::warning('Failed to create collection "' . $name . '": Collection already exists');
}
}
/**
* Creates attribute from collections.php
* Creates attributes from collections.php
*
* @param \Utopia\Database\Database $database
* @param Database $database
* @param string $collectionId
* @param string $attributeId
* @param array $attributeIds
* @param string|null $from
* @return void
* @throws \Exception
* @throws \Utopia\Database\Exception\Duplicate
* @throws \Utopia\Database\Exception\Limit
* @throws \Utopia\Database\Exception
* @throws \Utopia\Database\Exception\Authorization
* @throws Conflict
* @throws Duplicate
* @throws Limit
* @throws Structure
*/
public function createAttributeFromCollection(Database $database, string $collectionId, string $attributeId, string $from = null): void
{
public function createAttributesFromCollection(
Database $database,
string $collectionId,
array $attributeIds,
string $from = null
): void {
$from ??= $collectionId;
$collectionType = match ($this->project->getInternalId()) {
@@ -323,13 +271,78 @@ abstract class Migration
$collection = $this->collections[$collectionType][$from] ?? null;
if (is_null($collection)) {
if ($collection === null) {
throw new Exception("Collection {$from} not found");
}
$attributesToCreate = [];
$attributes = $collection['attributes'];
$attributeKeys = \array_column($collection['attributes'], '$id');
foreach ($attributeIds as $attributeId) {
$attributeKey = \array_search($attributeId, $attributeKeys);
if ($attributeKey === false) {
throw new Exception("Attribute {$attributeId} not found");
}
$attribute = $attributes[$attributeKey];
$attribute['filters'] ??= [];
$attribute['default'] ??= null;
$attribute['default'] = \in_array('json', $attribute['filters'])
? \json_encode($attribute['default'])
: $attribute['default'];
$attributesToCreate[] = $attribute;
}
$database->createAttributes(
collection: $collectionId,
attributes: $attributesToCreate,
);
}
/**
* Creates attribute from collections.php
*
* @param Database $database
* @param string $collectionId
* @param string $attributeId
* @param string|null $from
* @return void
* @throws \Utopia\Database\Exception
* @throws \Utopia\Database\Exception\Authorization
* @throws Conflict
* @throws Duplicate
* @throws Limit
* @throws Structure
*/
public function createAttributeFromCollection(
Database $database,
string $collectionId,
string $attributeId,
string $from = null
): void {
$from ??= $collectionId;
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
if ($from === 'files') {
$collectionType = 'buckets';
}
$collection = $this->collections[$collectionType][$from] ?? null;
if ($collection === null) {
throw new Exception("Collection {$from} not found");
}
$attributes = $collection['attributes'];
$attributeKey = array_search($attributeId, array_column($attributes, '$id'));
$attributeKey = \array_search($attributeId, \array_column($attributes, '$id'));
if ($attributeKey === false) {
throw new Exception("Attribute {$attributeId} not found");
@@ -344,9 +357,9 @@ abstract class Migration
id: $attributeId,
type: $attribute['type'],
size: $attribute['size'],
required: $attribute['required'] ?? false,
default: in_array('json', $filters) ? json_encode($default) : $default,
signed: $attribute['signed'] ?? false,
required: $attribute['required'],
default: \in_array('json', $filters) ? \json_encode($default) : $default,
signed: $attribute['signed'] ?? true,
array: $attribute['array'] ?? false,
format: $attribute['format'] ?? '',
formatOptions: $attribute['formatOptions'] ?? [],
@@ -357,14 +370,14 @@ abstract class Migration
/**
* Creates index from collections.php
*
* @param \Utopia\Database\Database $database
* @param Database $database
* @param string $collectionId
* @param string $indexId
* @param string|null $from
* @return void
* @throws \Exception
* @throws \Utopia\Database\Exception\Duplicate
* @throws \Utopia\Database\Exception\Limit
* @throws Duplicate
* @throws Limit
*/
public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void
{
@@ -381,13 +394,13 @@ abstract class Migration
$collection = $this->collections[$collectionType][$from] ?? null;
if (is_null($collection)) {
if ($collection === null) {
throw new Exception("Collection {$collectionId} not found");
}
$indexes = $collection['indexes'];
$indexKey = array_search($indexId, array_column($indexes, '$id'));
$indexKey = \array_search($indexId, \array_column($indexes, '$id'));
if ($indexKey === false) {
throw new Exception("Index {$indexId} not found");
@@ -412,10 +425,11 @@ abstract class Migration
* @param string $attribute
* @param string $type
* @return void
* @throws \Utopia\Database\Exception
*/
protected function changeAttributeInternalType(string $collection, string $attribute, string $type): void
{
$stmt = $this->pdo->prepare("ALTER TABLE `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$collection}` MODIFY `$attribute` $type;");
$stmt = $this->pdo->prepare("ALTER TABLE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$collection}` MODIFY `$attribute` $type;");
try {
$stmt->execute();
+80 -80
View File
@@ -114,7 +114,7 @@ class V15 extends Migration
$bucket->setAttribute('compression', 'none');
}
$this->projectDB->updateDocument('buckets', $bucket->getId(), $bucket);
$this->dbForProject->updateDocument('buckets', $bucket->getId(), $bucket);
/**
* Migrating stats for every Bucket.
@@ -134,13 +134,13 @@ class V15 extends Migration
table: $bucketTable,
addCreatePermission: false
);
$this->projectDB->updateDocument($bucketTable, $file->getId(), $file);
$this->dbForProject->updateDocument($bucketTable, $file->getId(), $file);
}
$this->removeWritePermissions($bucketTable);
}
try {
$this->projectDB->deleteAttribute('buckets', 'permission');
$this->dbForProject->deleteAttribute('buckets', 'permission');
} catch (\Throwable $th) {
Console::warning("'permissions' from buckets: {$th->getMessage()}");
}
@@ -188,10 +188,10 @@ class V15 extends Migration
addCreatePermission: false
);
$this->projectDB->updateDocument('databases', $database->getId(), $database);
$this->dbForProject->updateDocument('databases', $database->getId(), $database);
try {
$this->createAttributeFromCollection($this->projectDB, $databaseTable, 'documentSecurity', 'collections');
$this->createAttributeFromCollection($this->dbForProject, $databaseTable, 'documentSecurity', 'collections');
} catch (\Throwable $th) {
Console::warning("'documentSecurity' from {$databaseTable}: {$th->getMessage()}");
}
@@ -231,7 +231,7 @@ class V15 extends Migration
$collection->setAttribute('documentSecurity', $collection->getAttribute('permissions') === 'document');
}
$this->projectDB->updateDocument($databaseTable, $collection->getId(), $collection);
$this->dbForProject->updateDocument($databaseTable, $collection->getId(), $collection);
/**
* Migrating stats for single Collections.
@@ -270,14 +270,14 @@ class V15 extends Migration
addCreatePermission: false
);
$this->projectDB->updateDocument($collectionTable, $document->getId(), $document);
$this->dbForProject->updateDocument($collectionTable, $document->getId(), $document);
}
$this->removeWritePermissions($collectionTable);
}
$this->removeWritePermissions($databaseTable);
try {
$this->projectDB->deleteAttribute("database_{$database->getInternalId()}", 'permission');
$this->dbForProject->deleteAttribute("database_{$database->getInternalId()}", 'permission');
} catch (\Throwable $th) {
Console::warning("'permission' from {$databaseTable}: {$th->getMessage()}");
}
@@ -293,7 +293,7 @@ class V15 extends Migration
protected function removeWritePermissions(string $table): void
{
try {
$this->pdo->prepare("DELETE FROM `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}_perms` WHERE _type = 'write'")->execute();
$this->pdo->prepare("DELETE FROM `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}_perms` WHERE _type = 'write'")->execute();
} catch (\Throwable $th) {
Console::warning("Remove 'write' permissions from {$table}: {$th->getMessage()}");
}
@@ -309,7 +309,7 @@ class V15 extends Migration
*/
protected function getSQLColumnTypes(string $table): array
{
$query = $this->pdo->prepare("SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '_{$this->project->getInternalId()}_{$table}' AND table_schema = '{$this->projectDB->getDatabase()}'");
$query = $this->pdo->prepare("SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '_{$this->project->getInternalId()}_{$table}' AND table_schema = '{$this->dbForProject->getDatabase()}'");
$query->execute();
return array_reduce($query->fetchAll(), function (array $carry, array $item) {
@@ -331,8 +331,8 @@ class V15 extends Migration
if ($columns[$attribute] === 'int') {
try {
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` MODIFY {$attribute} VARCHAR(64)")->execute();
$this->pdo->prepare("UPDATE `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` SET {$attribute} = IF({$attribute} = 0, NULL, FROM_UNIXTIME({$attribute}))")->execute();
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` MODIFY {$attribute} VARCHAR(64)")->execute();
$this->pdo->prepare("UPDATE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` SET {$attribute} = IF({$attribute} = 0, NULL, FROM_UNIXTIME({$attribute}))")->execute();
$columns[$attribute] = 'varchar';
} catch (\Throwable $th) {
Console::warning($th->getMessage());
@@ -341,7 +341,7 @@ class V15 extends Migration
if ($columns[$attribute] === 'varchar') {
try {
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` MODIFY {$attribute} DATETIME(3)")->execute();
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` MODIFY {$attribute} DATETIME(3)")->execute();
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
@@ -355,11 +355,11 @@ class V15 extends Migration
/**
* Add datetime filter.
*/
$this->projectDB->updateAttributeFilters($table, ID::custom($attribute), ['datetime']);
$this->dbForProject->updateAttributeFilters($table, ID::custom($attribute), ['datetime']);
/**
* Change data type to DateTime.
*/
$this->projectDB->updateAttribute(
$this->dbForProject->updateAttribute(
collection: $table,
id: $attribute,
type: Database::VAR_DATETIME,
@@ -370,7 +370,7 @@ class V15 extends Migration
}
}
$this->projectDB->purgeCachedCollection($table);
$this->dbForProject->purgeCachedCollection($table);
}
/**
@@ -387,7 +387,7 @@ class V15 extends Migration
if (!array_key_exists('_permissions', $columns)) {
try {
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` ADD `_permissions` MEDIUMTEXT DEFAULT NULL")->execute();
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}` ADD `_permissions` MEDIUMTEXT DEFAULT NULL")->execute();
} catch (\Throwable $th) {
Console::warning("Add '_permissions' column to '{$table}': {$th->getMessage()}");
}
@@ -408,7 +408,7 @@ class V15 extends Migration
{
$table ??= $document->getCollection();
$query = $this->pdo->prepare("SELECT * FROM `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}_perms` WHERE _document = '{$document->getId()}'");
$query = $this->pdo->prepare("SELECT * FROM `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$table}_perms` WHERE _document = '{$document->getId()}'");
$query->execute();
$results = $query->fetchAll();
$permissions = [];
@@ -466,7 +466,7 @@ class V15 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case '_metadata':
@@ -477,7 +477,7 @@ class V15 extends Migration
$this->createCollection('cache');
Console::log('Created new Collection "variables" collection');
$this->createCollection('variables');
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'abuse':
@@ -509,7 +509,7 @@ class V15 extends Migration
/**
* Create 'compression' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'compression');
$this->createAttributeFromCollection($this->dbForProject, $id, 'compression');
} catch (\Throwable $th) {
Console::warning("'compression' from {$id}: {$th->getMessage()}");
}
@@ -518,7 +518,7 @@ class V15 extends Migration
/**
* Create 'fileSecurity' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'fileSecurity');
$this->createAttributeFromCollection($this->dbForProject, $id, 'fileSecurity');
} catch (\Throwable $th) {
Console::warning("'fileSecurity' from {$id}: {$th->getMessage()}");
}
@@ -527,7 +527,7 @@ class V15 extends Migration
/**
* Create '_key_enabled' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_enabled');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_enabled');
} catch (\Throwable $th) {
Console::warning("'_key_enabled' from {$id}: {$th->getMessage()}");
}
@@ -536,7 +536,7 @@ class V15 extends Migration
/**
* Create '_key_name' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_name');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_name');
} catch (\Throwable $th) {
Console::warning("'_key_name' from {$id}: {$th->getMessage()}");
}
@@ -545,7 +545,7 @@ class V15 extends Migration
/**
* Create '_key_fileSecurity' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_fileSecurity');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_fileSecurity');
} catch (\Throwable $th) {
Console::warning("'_key_fileSecurity' from {$id}: {$th->getMessage()}");
}
@@ -554,7 +554,7 @@ class V15 extends Migration
/**
* Create '_key_maximumFileSize' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_maximumFileSize');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_maximumFileSize');
} catch (\Throwable $th) {
Console::warning("'_key_maximumFileSize' from {$id}: {$th->getMessage()}");
}
@@ -563,7 +563,7 @@ class V15 extends Migration
/**
* Create '_key_encryption' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_encryption');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_encryption');
} catch (\Throwable $th) {
Console::warning("'_key_encryption' from {$id}: {$th->getMessage()}");
}
@@ -572,7 +572,7 @@ class V15 extends Migration
/**
* Create '_key_antivirus' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_antivirus');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_antivirus');
} catch (\Throwable $th) {
Console::warning("'_key_antivirus' from {$id}: {$th->getMessage()}");
}
@@ -611,7 +611,7 @@ class V15 extends Migration
/**
* Create '_key_entrypoint' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_entrypoint');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_entrypoint');
} catch (\Throwable $th) {
Console::warning("'_key_entrypoint' from {$id}: {$th->getMessage()}");
}
@@ -620,7 +620,7 @@ class V15 extends Migration
/**
* Create '_key_size' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_size');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_size');
} catch (\Throwable $th) {
Console::warning("'_key_size' from {$id}: {$th->getMessage()}");
}
@@ -629,7 +629,7 @@ class V15 extends Migration
/**
* Create '_key_buildId' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_buildId');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_buildId');
} catch (\Throwable $th) {
Console::warning("'_key_buildId' from {$id}: {$th->getMessage()}");
}
@@ -638,7 +638,7 @@ class V15 extends Migration
/**
* Create '_key_activate' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_activate');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_activate');
} catch (\Throwable $th) {
Console::warning("'_key_activate' from {$id}: {$th->getMessage()}");
}
@@ -662,7 +662,7 @@ class V15 extends Migration
/**
* Create 'stdout' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'stdout');
$this->createAttributeFromCollection($this->dbForProject, $id, 'stdout');
} catch (\Throwable $th) {
Console::warning("'stdout' from {$id}: {$th->getMessage()}");
}
@@ -671,7 +671,7 @@ class V15 extends Migration
/**
* Rename 'time' to 'duration'
*/
$this->projectDB->renameAttribute($id, 'time', 'duration');
$this->dbForProject->renameAttribute($id, 'time', 'duration');
} catch (\Throwable $th) {
Console::warning("'duration' from {$id}: {$th->getMessage()}");
}
@@ -680,7 +680,7 @@ class V15 extends Migration
/**
* Create '_key_trigger' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_trigger');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_trigger');
} catch (\Throwable $th) {
Console::warning("'_key_trigger' from {$id}: {$th->getMessage()}");
}
@@ -689,7 +689,7 @@ class V15 extends Migration
/**
* Create '_key_status' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_status');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_status');
} catch (\Throwable $th) {
Console::warning("'_key_status' from {$id}: {$th->getMessage()}");
}
@@ -698,7 +698,7 @@ class V15 extends Migration
/**
* Create '_key_statusCode' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_statusCode');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_statusCode');
} catch (\Throwable $th) {
Console::warning("'_key_statusCode' from {$id}: {$th->getMessage()}");
}
@@ -707,7 +707,7 @@ class V15 extends Migration
/**
* Create '_key_duration' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_duration');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_duration');
} catch (\Throwable $th) {
Console::warning("'_key_duration' from {$id}: {$th->getMessage()}");
}
@@ -751,16 +751,16 @@ class V15 extends Migration
'value' => (string) $value,
'search' => implode(' ', [$variableId, $key, $function->getId()])
]);
$this->projectDB->createDocument('variables', $variable);
$this->dbForProject->createDocument('variables', $variable);
}
$this->projectDB->deleteAttribute('functions', 'vars');
$this->createAttributeFromCollection($this->projectDB, 'functions', 'vars');
$this->dbForProject->deleteAttribute('functions', 'vars');
$this->createAttributeFromCollection($this->dbForProject, 'functions', 'vars');
}
try {
/**
* Create 'scheduleUpdatedAt' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleUpdatedAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'scheduleUpdatedAt');
} catch (\Throwable $th) {
Console::warning("'scheduleUpdatedAt' from {$id}: {$th->getMessage()}");
}
@@ -768,8 +768,8 @@ class V15 extends Migration
/**
* Create 'enabled' attribute
*/
@$this->projectDB->deleteAttribute($id, 'status');
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
@$this->dbForProject->deleteAttribute($id, 'status');
$this->createAttributeFromCollection($this->dbForProject, $id, 'enabled');
} catch (\Throwable $th) {
Console::warning("'enabled' from {$id}: {$th->getMessage()}");
}
@@ -777,7 +777,7 @@ class V15 extends Migration
/**
* Create '_key_name' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_name');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_name');
} catch (\Throwable $th) {
Console::warning("'_key_name' from {$id}: {$th->getMessage()}");
}
@@ -786,7 +786,7 @@ class V15 extends Migration
/**
* Create '_key_enabled' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_enabled');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_enabled');
} catch (\Throwable $th) {
Console::warning("'_key_enabled' from {$id}: {$th->getMessage()}");
}
@@ -795,7 +795,7 @@ class V15 extends Migration
/**
* Create '_key_runtime' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_runtime');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_runtime');
} catch (\Throwable $th) {
Console::warning("'_key_runtime' from {$id}: {$th->getMessage()}");
}
@@ -804,7 +804,7 @@ class V15 extends Migration
/**
* Create '_key_deployment' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_deployment');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_deployment');
} catch (\Throwable $th) {
Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}");
}
@@ -813,7 +813,7 @@ class V15 extends Migration
/**
* Create '_key_schedule' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_schedule');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_schedule');
} catch (\Throwable $th) {
Console::warning("'_key_schedule' from {$id}: {$th->getMessage()}");
}
@@ -822,7 +822,7 @@ class V15 extends Migration
/**
* Create '_key_scheduleNext' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_scheduleNext');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_scheduleNext');
} catch (\Throwable $th) {
Console::warning("'_key_scheduleNext' from {$id}: {$th->getMessage()}");
}
@@ -831,7 +831,7 @@ class V15 extends Migration
/**
* Create '_key_schedulePrevious' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_schedulePrevious');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_schedulePrevious');
} catch (\Throwable $th) {
Console::warning("'_key_schedulePrevious' from {$id}: {$th->getMessage()}");
}
@@ -840,7 +840,7 @@ class V15 extends Migration
/**
* Create '_key_timeout' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_timeout');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_timeout');
} catch (\Throwable $th) {
Console::warning("'_key_timeout' from {$id}: {$th->getMessage()}");
}
@@ -863,7 +863,7 @@ class V15 extends Migration
/**
* Update 'expire' default value
*/
$this->projectDB->updateAttributeDefault('keys', 'expire', null);
$this->dbForProject->updateAttributeDefault('keys', 'expire', null);
} catch (\Throwable $th) {
Console::warning("'expire' from {$id}: {$th->getMessage()}");
}
@@ -871,7 +871,7 @@ class V15 extends Migration
/**
* Create 'accessedAt' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'accessedAt');
} catch (\Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
@@ -880,7 +880,7 @@ class V15 extends Migration
/**
* Create 'sdks' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'sdks');
$this->createAttributeFromCollection($this->dbForProject, $id, 'sdks');
} catch (\Throwable $th) {
Console::warning("'sdks' from {$id}: {$th->getMessage()}");
}
@@ -889,7 +889,7 @@ class V15 extends Migration
/**
* Create '_key_accessedAt' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_accessedAt');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_accessedAt');
} catch (\Throwable $th) {
Console::warning("'_key_accessedAt' from {$id}: {$th->getMessage()}");
}
@@ -907,7 +907,7 @@ class V15 extends Migration
/**
* Create '_key_userId' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_userId');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_userId');
} catch (\Throwable $th) {
Console::warning("'_key_userId' from {$id}: {$th->getMessage()}");
}
@@ -916,7 +916,7 @@ class V15 extends Migration
/**
* Create '_key_teamId' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_teamId');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_teamId');
} catch (\Throwable $th) {
Console::warning("'_key_teamId' from {$id}: {$th->getMessage()}");
}
@@ -925,7 +925,7 @@ class V15 extends Migration
/**
* Create '_key_invited' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_invited');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_invited');
} catch (\Throwable $th) {
Console::warning("'_key_invited' from {$id}: {$th->getMessage()}");
}
@@ -934,7 +934,7 @@ class V15 extends Migration
/**
* Create '_key_joined' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_joined');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_joined');
} catch (\Throwable $th) {
Console::warning("'_key_joined' from {$id}: {$th->getMessage()}");
}
@@ -943,7 +943,7 @@ class V15 extends Migration
/**
* Create '_key_confirm' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_confirm');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_confirm');
} catch (\Throwable $th) {
Console::warning("'_key_confirm' from {$id}: {$th->getMessage()}");
}
@@ -966,7 +966,7 @@ class V15 extends Migration
/**
* Create '_key_name' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_name');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_name');
} catch (\Throwable $th) {
Console::warning("'_key_name' from {$id}: {$th->getMessage()}");
}
@@ -1000,8 +1000,8 @@ class V15 extends Migration
/**
* Re-Create '_key_metric' index
*/
@$this->projectDB->deleteIndex($id, '_key_metric');
$this->createIndexFromCollection($this->projectDB, $id, '_key_period_time');
@$this->dbForProject->deleteIndex($id, '_key_metric');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_period_time');
} catch (\Throwable $th) {
Console::warning("'_key_period_time' from {$id}: {$th->getMessage()}");
}
@@ -1010,8 +1010,8 @@ class V15 extends Migration
/**
* Re-Create '_key_metric_period' index
*/
@$this->projectDB->deleteIndex($id, '_key_metric_period');
$this->createIndexFromCollection($this->projectDB, $id, '_key_metric_period_time');
@$this->dbForProject->deleteIndex($id, '_key_metric_period');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_metric_period_time');
} catch (\Throwable $th) {
Console::warning("'_key_metric_period_time' from {$id}: {$th->getMessage()}");
}
@@ -1027,7 +1027,7 @@ class V15 extends Migration
/**
* Create '_key_name' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_name');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_name');
} catch (\Throwable $th) {
Console::warning("'_key_name' from {$id}: {$th->getMessage()}");
}
@@ -1036,7 +1036,7 @@ class V15 extends Migration
/**
* Create '_key_total' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_total');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_total');
} catch (\Throwable $th) {
Console::warning("'_key_total' from {$id}: {$th->getMessage()}");
}
@@ -1062,7 +1062,7 @@ class V15 extends Migration
/**
* Create 'hash' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'hash');
$this->createAttributeFromCollection($this->dbForProject, $id, 'hash');
} catch (\Throwable $th) {
Console::warning("'hash' from {$id}: {$th->getMessage()}");
}
@@ -1071,7 +1071,7 @@ class V15 extends Migration
/**
* Create 'hashOptions' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'hashOptions');
$this->createAttributeFromCollection($this->dbForProject, $id, 'hashOptions');
} catch (\Throwable $th) {
Console::warning("'hashOptions' from {$id}: {$th->getMessage()}");
}
@@ -1124,14 +1124,14 @@ class V15 extends Migration
*/
$this->populatePermissionsAttribute($user, addCreatePermission: false);
$this->projectDB->updateDocument('users', $user->getId(), $user);
$this->dbForProject->updateDocument('users', $user->getId(), $user);
}
try {
/**
* Add datetime filter to password.
*/
$this->projectDB->updateAttributeFilters($id, 'password', ['encrypt']);
$this->dbForProject->updateAttributeFilters($id, 'password', ['encrypt']);
} catch (\Throwable $th) {
Console::warning("Add 'encrypt' filter to 'password' from {$id}: {$th->getMessage()}");
}
@@ -1140,7 +1140,7 @@ class V15 extends Migration
/**
* Create '_key_name' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_name');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_name');
} catch (\Throwable $th) {
Console::warning("'_key_name' from {$id}: {$th->getMessage()}");
}
@@ -1149,7 +1149,7 @@ class V15 extends Migration
/**
* Create '_key_status' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_status');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_status');
} catch (\Throwable $th) {
Console::warning("'_key_status' from {$id}: {$th->getMessage()}");
}
@@ -1158,7 +1158,7 @@ class V15 extends Migration
/**
* Create '_key_passwordUpdate' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_passwordUpdate');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_passwordUpdate');
} catch (\Throwable $th) {
Console::warning("'_key_passwordUpdate' from {$id}: {$th->getMessage()}");
}
@@ -1167,7 +1167,7 @@ class V15 extends Migration
/**
* Create '_key_registration' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_registration');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_registration');
} catch (\Throwable $th) {
Console::warning("'_key_registration' from {$id}: {$th->getMessage()}");
}
@@ -1176,7 +1176,7 @@ class V15 extends Migration
/**
* Create '_key_emailVerification' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_emailVerification');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_emailVerification');
} catch (\Throwable $th) {
Console::warning("'_key_emailVerification' from {$id}: {$th->getMessage()}");
}
@@ -1185,7 +1185,7 @@ class V15 extends Migration
/**
* Create '_key_phoneVerification' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_phoneVerification');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_phoneVerification');
} catch (\Throwable $th) {
Console::warning("'_key_phoneVerification' from {$id}: {$th->getMessage()}");
}
@@ -1470,9 +1470,9 @@ class V15 extends Migration
$from = $this->pdo->quote($from);
$to = $this->pdo->quote($to);
$this->pdo->prepare("UPDATE `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_stats` SET metric = {$to} WHERE metric = {$from}")->execute();
$this->pdo->prepare("UPDATE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_stats` SET metric = {$to} WHERE metric = {$from}")->execute();
} catch (\Throwable $th) {
Console::warning("Migrating steps from {$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}_stats:" . $th->getMessage());
Console::warning("Migrating steps from {$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_stats:" . $th->getMessage());
}
}
+5 -5
View File
@@ -45,7 +45,7 @@ class V16 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case 'sessions':
@@ -53,7 +53,7 @@ class V16 extends Migration
/**
* Create 'expire' attribute
*/
$this->projectDB->deleteAttribute($id, 'expire');
$this->dbForProject->deleteAttribute($id, 'expire');
} catch (\Throwable $th) {
Console::warning("'expire' from {$id}: {$th->getMessage()}");
}
@@ -65,7 +65,7 @@ class V16 extends Migration
/**
* Create 'region' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'region');
$this->createAttributeFromCollection($this->dbForProject, $id, 'region');
} catch (\Throwable $th) {
Console::warning("'region' from {$id}: {$th->getMessage()}");
}
@@ -74,7 +74,7 @@ class V16 extends Migration
/**
* Create '_key_team' index
*/
$this->createIndexFromCollection($this->projectDB, $id, '_key_team');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_team');
} catch (\Throwable $th) {
Console::warning("'_key_team' from {$id}: {$th->getMessage()}");
}
@@ -85,7 +85,7 @@ class V16 extends Migration
/**
* Create 'region' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'region');
$this->createAttributeFromCollection($this->dbForProject, $id, 'region');
} catch (\Throwable $th) {
Console::warning("'region' from {$id}: {$th->getMessage()}");
}
+33 -33
View File
@@ -47,8 +47,8 @@ class V17 extends Migration
$id = "bucket_{$bucket->getInternalId()}";
try {
$this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
@@ -67,7 +67,7 @@ class V17 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case 'builds':
@@ -75,8 +75,8 @@ class V17 extends Migration
/**
* Create 'size' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'size');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'size');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'size' from {$id}: {$th->getMessage()}");
}
@@ -87,8 +87,8 @@ class V17 extends Migration
/**
* Update 'mimeType' attribute size (127->255)
*/
$this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
@@ -97,8 +97,8 @@ class V17 extends Migration
/**
* Create 'bucketInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'bucketInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'bucketInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
@@ -109,8 +109,8 @@ class V17 extends Migration
/**
* Delete 'endTime' attribute (use startTime+duration if needed)
*/
$this->projectDB->deleteAttribute($id, 'endTime');
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->deleteAttribute($id, 'endTime');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'endTime' from {$id}: {$th->getMessage()}");
}
@@ -119,8 +119,8 @@ class V17 extends Migration
/**
* Rename 'outputPath' to 'path'
*/
$this->projectDB->renameAttribute($id, 'outputPath', 'path');
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->renameAttribute($id, 'outputPath', 'path');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'path' from {$id}: {$th->getMessage()}");
}
@@ -129,8 +129,8 @@ class V17 extends Migration
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'deploymentInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
@@ -141,8 +141,8 @@ class V17 extends Migration
/**
* Delete 'type' attribute
*/
$this->projectDB->deleteAttribute($id, 'type');
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->deleteAttribute($id, 'type');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
@@ -153,8 +153,8 @@ class V17 extends Migration
/**
* Create 'resourceInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'resourceInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
@@ -165,8 +165,8 @@ class V17 extends Migration
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'deploymentInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
@@ -175,8 +175,8 @@ class V17 extends Migration
/**
* Create 'scheduleInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'scheduleInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}");
}
@@ -185,8 +185,8 @@ class V17 extends Migration
/**
* Delete 'scheduleUpdatedAt' attribute
*/
$this->projectDB->deleteAttribute($id, 'scheduleUpdatedAt');
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->deleteAttribute($id, 'scheduleUpdatedAt');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'scheduleUpdatedAt' from {$id}: {$th->getMessage()}");
}
@@ -197,8 +197,8 @@ class V17 extends Migration
/**
* Create 'resourceInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'resourceInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
@@ -207,8 +207,8 @@ class V17 extends Migration
/**
* Create 'buildInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'buildInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'buildInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'buildInternalId' from {$id}: {$th->getMessage()}");
}
@@ -219,8 +219,8 @@ class V17 extends Migration
/**
* Create 'functionInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'functionInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'functionInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'functionInternalId' from {$id}: {$th->getMessage()}");
}
@@ -229,8 +229,8 @@ class V17 extends Migration
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'deploymentInternalId');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
+15 -15
View File
@@ -26,7 +26,7 @@ class V18 extends Migration
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
$this->addDocumentSecurityToProject();
Console::info('Migrating Databases');
@@ -66,7 +66,7 @@ class V18 extends Migration
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$permissions = $collection->getPermissions();
$this->projectDB->updateCollection($collectionTable, $permissions, $documentSecurity);
$this->dbForProject->updateCollection($collectionTable, $permissions, $documentSecurity);
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
@@ -94,7 +94,7 @@ class V18 extends Migration
}
try {
$this->projectDB->updateCollection($id, [Permission::create(Role::any())], true);
$this->dbForProject->updateCollection($id, [Permission::create(Role::any())], true);
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
@@ -105,8 +105,8 @@ class V18 extends Migration
/**
* Create 'passwordHistory' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'passwordHistory');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'passwordHistory');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'passwordHistory' from {$id}: {$th->getMessage()}");
}
@@ -116,8 +116,8 @@ class V18 extends Migration
/**
* Create 'prefs' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'prefs');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'prefs');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'prefs' from {$id}: {$th->getMessage()}");
}
@@ -127,8 +127,8 @@ class V18 extends Migration
/**
* Create 'options' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'options');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'options');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'options' from {$id}: {$th->getMessage()}");
}
@@ -138,7 +138,7 @@ class V18 extends Migration
/**
* Delete 'userInternalId' attribute
*/
$this->projectDB->deleteAttribute($id, 'userInternalId');
$this->dbForProject->deleteAttribute($id, 'userInternalId');
} catch (\Throwable $th) {
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
}
@@ -200,7 +200,7 @@ class V18 extends Migration
$internalBucketId = "bucket_{$this->project->getInternalId()}";
$permissions = $document->getPermissions();
$fileSecurity = $document->getAttribute('fileSecurity', false);
$this->projectDB->updateCollection($internalBucketId, $permissions, $fileSecurity);
$this->dbForProject->updateCollection($internalBucketId, $permissions, $fileSecurity);
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
@@ -214,8 +214,8 @@ class V18 extends Migration
$data = $document->getAttribute('data', []);
$mode = $data['mode'] ?? 'default';
$user = match ($mode) {
'admin' => $this->consoleDB->getDocument('users', $userId),
default => $this->projectDB->getDocument('users', $userId),
'admin' => $this->dbForPlatform->getDocument('users', $userId),
default => $this->dbForProject->getDocument('users', $userId),
};
if ($user->isEmpty()) {
@@ -244,7 +244,7 @@ class V18 extends Migration
/**
* Create 'documentSecurity' column
*/
$this->pdo->prepare("ALTER TABLE `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}__metadata` ADD COLUMN IF NOT EXISTS documentSecurity TINYINT(1);")->execute();
$this->pdo->prepare("ALTER TABLE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}__metadata` ADD COLUMN IF NOT EXISTS documentSecurity TINYINT(1);")->execute();
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
@@ -253,7 +253,7 @@ class V18 extends Migration
/**
* Set 'documentSecurity' column to 1 if NULL
*/
$this->pdo->prepare("UPDATE `{$this->projectDB->getDatabase()}`.`_{$this->project->getInternalId()}__metadata` SET documentSecurity = 1 WHERE documentSecurity IS NULL")->execute();
$this->pdo->prepare("UPDATE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}__metadata` SET documentSecurity = 1 WHERE documentSecurity IS NULL")->execute();
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
+77 -77
View File
@@ -28,7 +28,7 @@ class V19 extends Migration
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
@@ -55,7 +55,7 @@ class V19 extends Migration
protected function migrateDomains(): void
{
if ($this->consoleDB->exists($this->consoleDB->getDatabase(), 'domains')) {
if ($this->dbForPlatform->exists($this->dbForPlatform->getDatabase(), 'domains')) {
foreach ($this->documentsIterator('domains') as $domain) {
$status = 'created';
if ($domain->getAttribute('verification', false)) {
@@ -82,7 +82,7 @@ class V19 extends Migration
]);
try {
$this->consoleDB->createDocument('rules', $ruleDocument);
$this->dbForPlatform->createDocument('rules', $ruleDocument);
} catch (\Throwable $th) {
Console::warning("Error migrating domain {$domain->getAttribute('domain')}: {$th->getMessage()}");
}
@@ -104,8 +104,8 @@ class V19 extends Migration
Console::log("Migrating Bucket {$id} {$bucket->getId()} ({$bucket->getAttribute('name')})");
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'bucketInternalId', 'files');
$this->projectDB->purgeCachedCollection($id);
$this->createAttributeFromCollection($this->dbForProject, $id, 'bucketInternalId', 'files');
$this->dbForProject->purgeCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'bucketInternalId' from {$id}: {$th->getMessage()}");
}
@@ -136,7 +136,7 @@ class V19 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
$this->dbForProject->setNamespace("_$internalProjectId");
switch ($id) {
case '_metadata':
@@ -148,24 +148,24 @@ class V19 extends Migration
case 'attributes':
case 'indexes':
try {
$this->projectDB->updateAttribute($id, 'databaseInternalId', required: true);
$this->dbForProject->updateAttribute($id, 'databaseInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'databaseInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'collectionInternalId', required: true);
$this->dbForProject->updateAttribute($id, 'collectionInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'collectionInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'error');
$this->createAttributeFromCollection($this->dbForProject, $id, 'error');
} catch (\Throwable $th) {
Console::warning("'error' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'buckets':
@@ -175,7 +175,7 @@ class V19 extends Migration
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -187,13 +187,13 @@ class V19 extends Migration
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'builds':
@@ -204,45 +204,45 @@ class V19 extends Migration
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("$attribute from {$id}: {$th->getMessage()}");
}
}
try {
$this->projectDB->renameAttribute($id, 'outputPath', 'path');
$this->dbForProject->renameAttribute($id, 'outputPath', 'path');
} catch (\Throwable $th) {
Console::warning("'path' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'certificates':
try {
$this->projectDB->renameAttribute($id, 'log', 'logs');
$this->dbForProject->renameAttribute($id, 'log', 'logs');
} catch (\Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'logs', size: 1000000);
$this->dbForProject->updateAttribute($id, 'logs', size: 1000000);
} catch (\Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'databases':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
$this->createAttributeFromCollection($this->dbForProject, $id, 'enabled');
} catch (\Throwable $th) {
Console::warning("'enabled' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'deployments':
@@ -271,7 +271,7 @@ class V19 extends Migration
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("$attribute from {$id}: {$th->getMessage()}");
}
@@ -286,7 +286,7 @@ class V19 extends Migration
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -299,13 +299,13 @@ class V19 extends Migration
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'executions':
@@ -319,7 +319,7 @@ class V19 extends Migration
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("$attribute from {$id}: {$th->getMessage()}");
}
@@ -330,43 +330,43 @@ class V19 extends Migration
];
foreach ($attributesToDelete as $attribute) {
try {
$this->projectDB->deleteAttribute($id, $attribute);
$this->dbForProject->deleteAttribute($id, $attribute);
} catch (\Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
}
}
try {
$this->projectDB->renameAttribute($id, 'stderr', 'errors');
$this->dbForProject->renameAttribute($id, 'stderr', 'errors');
} catch (\Throwable $th) {
Console::warning("'errors' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'stdout', 'logs');
$this->dbForProject->renameAttribute($id, 'stdout', 'logs');
} catch (\Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'statusCode', 'responseStatusCode');
$this->dbForProject->renameAttribute($id, 'statusCode', 'responseStatusCode');
} catch (\Throwable $th) {
Console::warning("'responseStatusCode' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->deleteIndex($id, '_key_statusCode');
$this->dbForProject->deleteIndex($id, '_key_statusCode');
} catch (\Throwable $th) {
Console::warning("'_key_statusCode' from {$id}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_responseStatusCode');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_responseStatusCode');
} catch (\Throwable $th) {
Console::warning("'_key_responseStatusCode' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'files':
@@ -378,7 +378,7 @@ class V19 extends Migration
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -387,13 +387,13 @@ class V19 extends Migration
$indexesToCreate = $indexesToDelete;
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'functions':
@@ -419,7 +419,7 @@ class V19 extends Migration
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
}
@@ -433,7 +433,7 @@ class V19 extends Migration
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -449,34 +449,34 @@ class V19 extends Migration
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'memberships':
try {
$this->projectDB->updateAttribute($id, 'teamInternalId', required: true);
$this->dbForProject->updateAttribute($id, 'teamInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'teamInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
// Intentional fall through to update memberships.userInternalId
case 'sessions':
case 'tokens':
try {
$this->projectDB->updateAttribute($id, 'userInternalId', required: true);
$this->dbForProject->updateAttribute($id, 'userInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'domains':
@@ -484,12 +484,12 @@ class V19 extends Migration
case 'platforms':
case 'webhooks':
try {
$this->projectDB->updateAttribute($id, 'projectInternalId', required: true);
$this->dbForProject->updateAttribute($id, 'projectInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'projectInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'projects':
@@ -500,19 +500,19 @@ class V19 extends Migration
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
Console::warning($th->getTraceAsString());
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'stats':
try {
$this->projectDB->updateAttribute($id, 'value', signed: true);
$this->dbForProject->updateAttribute($id, 'value', signed: true);
} catch (\Throwable $th) {
Console::warning("'value' from {$id}: {$th->getMessage()}");
}
@@ -539,64 +539,64 @@ class V19 extends Migration
// Console::warning("'_key_metric_period_time' from {$id}: {$th->getMessage()}");
// }
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'users':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'labels');
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
} catch (\Throwable $th) {
Console::warning("'labels' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'accessedAt');
} catch (\Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'search', filters: ['userSearch']);
$this->dbForProject->updateAttribute($id, 'search', filters: ['userSearch']);
} catch (\Throwable $th) {
Console::warning("'search' from {$id}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_accessedAt');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_accessedAt');
} catch (\Throwable $th) {
Console::warning("'_key_accessedAt' from {$id}: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
case 'variables':
try {
$this->projectDB->deleteIndex($id, '_key_function');
$this->dbForProject->deleteIndex($id, '_key_function');
} catch (\Throwable $th) {
Console::warning("'_key_function' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->deleteIndex($id, '_key_uniqueKey');
$this->dbForProject->deleteIndex($id, '_key_uniqueKey');
} catch (\Throwable $th) {
Console::warning("'_key_uniqueKey' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceType');
$this->createAttributeFromCollection($this->dbForProject, $id, 'resourceType');
} catch (\Throwable $th) {
Console::warning("'resourceType' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'functionInternalId', 'resourceInternalId');
$this->dbForProject->renameAttribute($id, 'functionInternalId', 'resourceInternalId');
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'functionId', 'resourceId');
$this->dbForProject->renameAttribute($id, 'functionId', 'resourceId');
} catch (\Throwable $th) {
Console::warning("'resourceId' from {$id}: {$th->getMessage()}");
}
@@ -609,13 +609,13 @@ class V19 extends Migration
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
break;
default:
@@ -654,10 +654,10 @@ class V19 extends Migration
]) as $attribute
) {
$attribute->setAttribute('size', Database::LENGTH_KEY);
$this->projectDB->updateDocument('attributes', $attribute->getId(), $attribute);
$this->dbForProject->updateDocument('attributes', $attribute->getId(), $attribute);
$databaseInternalId = $attribute->getAttribute('databaseInternalId');
$collectionInternalId = $attribute->getAttribute('collectionInternalId');
$this->projectDB->updateAttribute('database_' . $databaseInternalId . '_collection_' . $collectionInternalId, $attribute->getAttribute('key'), size: 255);
$this->dbForProject->updateAttribute('database_' . $databaseInternalId . '_collection_' . $collectionInternalId, $attribute->getAttribute('key'), size: 255);
}
}
@@ -679,7 +679,7 @@ class V19 extends Migration
break;
case 'builds':
$deploymentId = $document->getAttribute('deploymentId');
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$deployment = $this->dbForProject->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
$stdout = $document->getAttribute('stdout', '');
@@ -691,12 +691,12 @@ class V19 extends Migration
break;
case 'deployments':
$resourceId = $document->getAttribute('resourceId');
$function = $this->projectDB->getDocument('functions', $resourceId);
$function = $this->dbForProject->getDocument('functions', $resourceId);
$document->setAttribute('resourceInternalId', $function->getInternalId());
$buildId = $document->getAttribute('buildId');
if (!empty($buildId)) {
$build = $this->projectDB->getDocument('builds', $buildId);
$build = $this->dbForProject->getDocument('builds', $buildId);
$document->setAttribute('buildInternalId', $build->getInternalId());
}
@@ -706,11 +706,11 @@ class V19 extends Migration
break;
case 'executions':
$functionId = $document->getAttribute('functionId');
$function = $this->projectDB->getDocument('functions', $functionId);
$function = $this->dbForProject->getDocument('functions', $functionId);
$document->setAttribute('functionInternalId', $function->getInternalId());
$deploymentId = $document->getAttribute('deploymentId');
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$deployment = $this->dbForProject->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
break;
case 'functions':
@@ -720,7 +720,7 @@ class V19 extends Migration
$deploymentId = $document->getAttribute('deployment');
if (!empty($deploymentId)) {
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$deployment = $this->dbForProject->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
$document->setAttribute('entrypoint', $deployment->getAttribute('entrypoint'));
}
@@ -729,7 +729,7 @@ class V19 extends Migration
$document->setAttribute('commands', $document->getAttribute('commands', $commands));
if (empty($document->getAttribute('scheduleId', null))) {
$schedule = $this->consoleDB->createDocument('schedules', new Document([
$schedule = $this->dbForPlatform->createDocument('schedules', new Document([
'region' => $project->getAttribute('region'),
'resourceType' => 'function',
'resourceId' => $document->getId(),
@@ -769,26 +769,26 @@ class V19 extends Migration
private function cleanCollections(): void
{
try {
$this->projectDB->deleteAttribute('projects', 'domains');
$this->dbForProject->deleteAttribute('projects', 'domains');
} catch (\Throwable $th) {
Console::warning("'domains' from projects: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection('projects');
$this->dbForProject->purgeCachedCollection('projects');
try {
$this->projectDB->deleteAttribute('builds', 'stderr');
$this->dbForProject->deleteAttribute('builds', 'stderr');
} catch (\Throwable $th) {
Console::warning("'stderr' from builds: {$th->getMessage()}");
}
try {
$this->projectDB->deleteAttribute('builds', 'stdout');
$this->dbForProject->deleteAttribute('builds', 'stdout');
} catch (\Throwable $th) {
Console::warning("'stdout' from builds: {$th->getMessage()}");
}
$this->projectDB->purgeCachedCollection('builds');
$this->dbForProject->purgeCachedCollection('builds');
}
/**
@@ -829,7 +829,7 @@ class V19 extends Migration
}
try {
$this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
$this->dbForProject->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
+46 -46
View File
@@ -36,7 +36,7 @@ class V20 extends Migration
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
@@ -94,19 +94,19 @@ class V20 extends Migration
) {
if (\in_array($attribute->getAttribute('key'), $index->getAttribute('attributes'))) {
try {
$this->projectDB->deleteIndex($collectionId, $index->getAttribute('key'));
$this->dbForProject->deleteIndex($collectionId, $index->getAttribute('key'));
} catch (Throwable $th) {
Console::warning("Failed to delete index: {$th->getMessage()}");
}
try {
$this->projectDB->deleteDocument('indexes', $index->getId());
$this->dbForProject->deleteDocument('indexes', $index->getId());
} catch (Throwable $th) {
Console::warning("Failed to remove index: {$th->getMessage()}");
}
}
}
$this->projectDB->updateAttribute($collectionId, $attribute['key'], $attribute['type']);
$this->dbForProject->updateAttribute($collectionId, $attribute['key'], $attribute['type']);
}
}
@@ -116,19 +116,19 @@ class V20 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
$this->dbForProject->setNamespace("_$internalProjectId");
// Support database array type migration
foreach ($collection['attributes'] ?? [] as $attribute) {
if ($attribute['array'] === true) {
foreach ($collection['indexes'] ?? [] as $index) {
if (\in_array($attribute['$id'], $index['attributes'])) {
$this->projectDB->deleteIndex($id, $index['$id']);
$this->dbForProject->deleteIndex($id, $index['$id']);
}
}
try {
$this->projectDB->updateAttribute($id, $attribute['$id'], $attribute['type']);
$this->dbForProject->updateAttribute($id, $attribute['$id'], $attribute['type']);
} catch (Throwable $th) {
Console::warning("'{$attribute['$id']}' from {$id}: {$th->getMessage()}");
}
@@ -151,19 +151,19 @@ class V20 extends Migration
// Create resourceType attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceType');
$this->createAttributeFromCollection($this->dbForProject, $id, 'resourceType');
} catch (Throwable $th) {
Console::warning("'resourceType' from {$id}: {$th->getMessage()}");
}
// Create mimeType attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mimeType');
$this->createAttributeFromCollection($this->dbForProject, $id, 'mimeType');
} catch (Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -174,11 +174,11 @@ class V20 extends Migration
/**
* Delete 'type' attribute
*/
$this->projectDB->deleteAttribute($id, 'type');
$this->dbForProject->deleteAttribute($id, 'type');
/**
* Alter `signed` internal type on `value` attr
*/
$this->projectDB->updateAttribute(collection: $id, id: 'value', signed: true);
$this->dbForProject->updateAttribute(collection: $id, id: 'value', signed: true);
} catch (Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
@@ -187,13 +187,13 @@ class V20 extends Migration
/**
* Ensure 'time' attribute is not required
*/
$this->projectDB->updateAttribute($id, 'time', required: false);
$this->dbForProject->updateAttribute($id, 'time', required: false);
} catch (Throwable $th) {
Console::warning("'time' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -202,13 +202,13 @@ class V20 extends Migration
$index = '_key_metric_period_time';
try {
$this->projectDB->deleteIndex($id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -217,27 +217,27 @@ class V20 extends Migration
case 'sessions':
// Create expire attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'expire');
$this->createAttributeFromCollection($this->dbForProject, $id, 'expire');
} catch (Throwable $th) {
Console::warning("'expire' from {$id}: {$th->getMessage()}");
}
// Create factors attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'factors');
$this->createAttributeFromCollection($this->dbForProject, $id, 'factors');
} catch (Throwable $th) {
Console::warning("'factors' from {$id}: {$th->getMessage()}");
}
// Create mfaRecoveryCodes attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mfaUpdatedAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'mfaUpdatedAt');
} catch (Throwable $th) {
Console::warning("'mfaUpdatedAt' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -246,41 +246,41 @@ class V20 extends Migration
case 'users':
// Create targets attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'targets');
$this->createAttributeFromCollection($this->dbForProject, $id, 'targets');
} catch (Throwable $th) {
Console::warning("'targets' from {$id}: {$th->getMessage()}");
}
// Create mfa attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mfa');
$this->createAttributeFromCollection($this->dbForProject, $id, 'mfa');
} catch (Throwable $th) {
Console::warning("'mfa' from {$id}: {$th->getMessage()}");
}
// Create mfaRecoveryCodes attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mfaRecoveryCodes');
$this->createAttributeFromCollection($this->dbForProject, $id, 'mfaRecoveryCodes');
} catch (Throwable $th) {
Console::warning("'mfaRecoveryCodes' from {$id}: {$th->getMessage()}");
}
// Create challenges attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'challenges');
$this->createAttributeFromCollection($this->dbForProject, $id, 'challenges');
} catch (Throwable $th) {
Console::warning("'challenges' from {$id}: {$th->getMessage()}");
}
// Create authenticators attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'authenticators');
$this->createAttributeFromCollection($this->dbForProject, $id, 'authenticators');
} catch (Throwable $th) {
Console::warning("'authenticators' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -289,20 +289,20 @@ class V20 extends Migration
case 'projects':
// Rename providers authProviders to oAuthProviders
try {
$this->projectDB->renameAttribute($id, 'authProviders', 'oAuthProviders');
$this->dbForProject->renameAttribute($id, 'authProviders', 'oAuthProviders');
} catch (Throwable $th) {
Console::warning("'oAuthProviders' from {$id}: {$th->getMessage()}");
}
// Create apis attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'apis');
$this->createAttributeFromCollection($this->dbForProject, $id, 'apis');
} catch (Throwable $th) {
Console::warning("'apis' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -311,27 +311,27 @@ class V20 extends Migration
case 'webhooks':
// Create enabled attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
$this->createAttributeFromCollection($this->dbForProject, $id, 'enabled');
} catch (Throwable $th) {
Console::warning("'enabled' from {$id}: {$th->getMessage()}");
}
// Create logs attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'logs');
$this->createAttributeFromCollection($this->dbForProject, $id, 'logs');
} catch (Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
// Create attempts attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'attempts');
$this->createAttributeFromCollection($this->dbForProject, $id, 'attempts');
} catch (Throwable $th) {
Console::warning("'attempts' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -339,25 +339,25 @@ class V20 extends Migration
break;
case 'topics':
try {
$this->projectDB->updateAttributeDefault($id, 'emailTotal', 0);
$this->dbForProject->updateAttributeDefault($id, 'emailTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'pushTotal', 0);
$this->dbForProject->updateAttributeDefault($id, 'pushTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'smsTotal', 0);
$this->dbForProject->updateAttributeDefault($id, 'smsTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
$this->dbForProject->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
@@ -382,7 +382,7 @@ class V20 extends Migration
*/
Console::info('Migrating Sessions metric');
$sessionsCreated = $this->projectDB->sum('stats', 'value', [
$sessionsCreated = $this->dbForProject->sum('stats', 'value', [
Query::equal('metric', [
'sessions.email-password.requests.create',
'sessions.magic-url.requests.create',
@@ -394,7 +394,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
$query = $this->projectDB->findOne('stats', [
$query = $this->dbForProject->findOne('stats', [
Query::equal('metric', ['sessions.$all.requests.delete']),
Query::equal('period', ['1d']),
]);
@@ -420,7 +420,7 @@ class V20 extends Migration
*/
Console::log("Creating inf metric to {$metric}");
$id = \md5("_inf_{$metric}");
$this->projectDB->createDocument('stats', new Document([
$this->dbForProject->createDocument('stats', new Document([
'$id' => $id,
'metric' => $metric,
'period' => 'inf',
@@ -448,7 +448,7 @@ class V20 extends Migration
str_contains($from, '$all') ||
str_contains($from, '.total')
) {
$query = $this->projectDB->sum('stats', 'value', [
$query = $this->dbForProject->sum('stats', 'value', [
Query::equal('metric', [$from]),
Query::equal('period', ['1d']),
]);
@@ -470,7 +470,7 @@ class V20 extends Migration
if ($latestDocument !== null) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$stats = $this->projectDB->find('stats', \array_merge($paginationQueries, [
$stats = $this->dbForProject->find('stats', \array_merge($paginationQueries, [
Query::equal('metric', [$from]),
]));
@@ -479,10 +479,10 @@ class V20 extends Migration
foreach ($stats as $stat) {
$format = $stat['period'] === '1d' ? 'Y-m-d 00:00' : 'Y-m-d H:00';
$time = date($format, strtotime($stat['time']));
$this->projectDB->deleteDocument('stats', $stat->getId());
$this->dbForProject->deleteDocument('stats', $stat->getId());
$stat->setAttribute('$id', \md5("{$time}_{$stat['period']}_{$to}"));
$stat->setAttribute('metric', $to);
$this->projectDB->createDocument('stats', $stat);
$this->dbForProject->createDocument('stats', $stat);
Console::log("deleting metric {$from} and creating {$to}");
}
$latestDocument = !empty(array_key_last($stats)) ? $stats[array_key_last($stats)] : null;
@@ -610,7 +610,7 @@ class V20 extends Migration
'identifier' => $document->getAttribute('email'),
]);
try {
$this->projectDB->createDocument('targets', $target);
$this->dbForProject->createDocument('targets', $target);
} catch (Duplicate $th) {
Console::warning("Email target for user {$document->getId()} already exists.");
}
@@ -625,7 +625,7 @@ class V20 extends Migration
'identifier' => $document->getAttribute('phone'),
]);
try {
$this->projectDB->createDocument('targets', $target);
$this->dbForProject->createDocument('targets', $target);
} catch (Duplicate $th) {
Console::warning("Email target for user {$document->getId()} already exists.");
}
+25 -25
View File
@@ -29,7 +29,7 @@ class V21 extends Migration
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
$this->dbForProject->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
@@ -63,13 +63,13 @@ class V21 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
$this->dbForProject->setNamespace("_$internalProjectId");
switch ($id) {
case 'projects':
// Create accessedAt attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'accessedAt');
} catch (Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
@@ -79,7 +79,7 @@ class V21 extends Migration
foreach ($attributesToCreate as $attribute) {
// Create attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
}
@@ -89,7 +89,7 @@ class V21 extends Migration
foreach ($indexesToCreate as $index) {
// Create index
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
@@ -98,7 +98,7 @@ class V21 extends Migration
case 'platforms':
// Increase 'type' length to 255
try {
$this->projectDB->updateAttribute($id, 'type', size: 255);
$this->dbForProject->updateAttribute($id, 'type', size: 255);
} catch (Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
@@ -108,7 +108,7 @@ class V21 extends Migration
foreach ($attributesToCreate as $attribute) {
// Create attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
$this->createAttributeFromCollection($this->dbForProject, $id, $attribute);
} catch (Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
}
@@ -117,7 +117,7 @@ class V21 extends Migration
case 'migrations':
// Create destination attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'destination');
$this->createAttributeFromCollection($this->dbForProject, $id, 'destination');
} catch (Throwable $th) {
Console::warning("'destination' from {$id}: {$th->getMessage()}");
}
@@ -125,7 +125,7 @@ class V21 extends Migration
case 'schedules':
// Create data attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'data');
$this->createAttributeFromCollection($this->dbForProject, $id, 'data');
} catch (Throwable $th) {
Console::warning("'data' from {$id}: {$th->getMessage()}");
}
@@ -134,7 +134,7 @@ class V21 extends Migration
case 'databases':
// Create originalId attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'originalId');
$this->createAttributeFromCollection($this->dbForProject, $id, 'originalId');
} catch (Throwable $th) {
Console::warning("'originalId' from {$id}: {$th->getMessage()}");
}
@@ -142,14 +142,14 @@ class V21 extends Migration
case 'functions':
// Create scopes attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'scopes');
$this->createAttributeFromCollection($this->dbForProject, $id, 'scopes');
} catch (Throwable $th) {
Console::warning("'scopes' from {$id}: {$th->getMessage()}");
}
// Create specification attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'specification');
$this->createAttributeFromCollection($this->dbForProject, $id, 'specification');
} catch (Throwable $th) {
Console::warning("'specification' from {$id}: {$th->getMessage()}");
}
@@ -158,21 +158,21 @@ class V21 extends Migration
case 'executions':
// Create requestMethod index
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_requestMethod');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_requestMethod');
} catch (\Throwable $th) {
Console::warning("'_key_requestMethod' from {$id}: {$th->getMessage()}");
}
// Create requestPath index
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_requestPath');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_requestPath');
} catch (\Throwable $th) {
Console::warning("'_key_requestPath' from {$id}: {$th->getMessage()}");
}
// Create deployment index
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_deployment');
$this->createIndexFromCollection($this->dbForProject, $id, '_key_deployment');
} catch (\Throwable $th) {
Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}");
}
@@ -181,7 +181,7 @@ class V21 extends Migration
/**
* Create 'scheduledAt' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduledAt');
$this->createAttributeFromCollection($this->dbForProject, $id, 'scheduledAt');
} catch (\Throwable $th) {
Console::warning("'scheduledAt' from {$id}: {$th->getMessage()}");
}
@@ -190,7 +190,7 @@ class V21 extends Migration
/**
* Create 'scheduleInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId');
$this->createAttributeFromCollection($this->dbForProject, $id, 'scheduleInternalId');
} catch (\Throwable $th) {
Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}");
}
@@ -199,7 +199,7 @@ class V21 extends Migration
/**
* Create 'scheduleId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleId');
$this->createAttributeFromCollection($this->dbForProject, $id, 'scheduleId');
} catch (\Throwable $th) {
Console::warning("'scheduleId' from {$id}: {$th->getMessage()}");
}
@@ -236,7 +236,7 @@ class V21 extends Migration
// Set specification attribute
if (empty($document->getAttribute('specification'))) {
$document->setAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT);
$document->setAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT);
}
}
@@ -250,34 +250,34 @@ class V21 extends Migration
*/
private function migrateBuckets(): void
{
foreach ($this->documentsIterator('buckets') as $bucket) {
$this->dbForProject->forEach('buckets', function (Document $bucket) {
$bucketId = 'bucket_' . $bucket['$internalId'];
Console::log("Migrating Bucket {$bucketId} {$bucket->getId()} ({$bucket->getAttribute('name')})");
try {
$this->projectDB->updateAttribute($bucketId, 'metadata', size: 65534);
$this->dbForProject->updateAttribute($bucketId, 'metadata', size: 65534);
} catch (\Throwable $th) {
Console::warning("'metadata' from {$bucketId}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $bucketId, 'transformedAt', 'files');
$this->createAttributeFromCollection($this->dbForProject, $bucketId, 'transformedAt', 'files');
} catch (\Throwable $th) {
Console::warning("'transformedAt' from {$bucketId}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $bucketId, '_key_transformedAt', 'files');
$this->createIndexFromCollection($this->dbForProject, $bucketId, '_key_transformedAt', 'files');
} catch (\Throwable $th) {
Console::warning("'_key_transformedAt' from {$bucketId}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($bucketId);
$this->dbForProject->purgeCachedCollection($bucketId);
} catch (\Throwable $th) {
Console::warning("purging {$bucketId}: {$th->getMessage()}");
}
}
});
}
}
+654
View File
@@ -0,0 +1,654 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Exception\Timeout;
use Utopia\Database\Query;
use Utopia\System\System;
class V22 extends Migration
{
/**
* @throws Throwable
*/
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryDevKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
);
}
Console::info('Migrating collections');
$this->migrateCollections();
Console::info('Migrating documents');
$this->forEachDocument($this->migrateDocument(...));
Console::info('Cleaning up collections');
$this->cleanCollections();
}
/**
* Migrate Collections.
*
* @return void
* @throws Exception|Throwable
*/
private function migrateCollections(): void
{
$projectInternalId = $this->project->getInternalId();
if (empty($projectInternalId)) {
throw new Exception('Project ID is null');
}
$collectionType = match ($projectInternalId) {
'console' => 'console',
default => 'projects',
};
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
if (empty($id)) {
continue;
}
Console::log("Migrating collection \"{$id}\"");
switch ($id) {
case '_metadata':
$this->createCollection('sites');
$this->createCollection('resourceTokens');
if ($projectInternalId === 'console') {
$this->createCollection('devKeys');
}
break;
case 'identities':
$attributes = [
'scopes',
'expire',
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'projects':
try {
$attributes = [
'devKeys',
];
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'rules':
$attributes = [
'type',
'trigger',
'redirectUrl',
'redirectStatusCode',
'deploymentResourceType',
'deploymentId',
'deploymentInternalId',
'deploymentResourceId',
'deploymentResourceInternalId',
'deploymentVcsProviderBranch',
'search'
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$indexes = [
'_key_search',
'_key_type',
'_key_trigger',
'_key_deploymentResourceType',
'_key_deploymentResourceId',
'_key_deploymentResourceInternalId',
'_key_deploymentId',
'_key_deploymentInternalId',
'_key_deploymentVcsProviderBranch',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'memberships':
$indexes = [
'_key_roles',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'migrations':
$attributes = [
'options',
'resourceId',
'resourceType'
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$indexes = [
'_key_resource_id',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'functions':
$attributes = [
'deploymentId',
'deploymentCreatedAt',
'latestDeploymentId',
'latestDeploymentInternalId',
'latestDeploymentCreatedAt',
'latestDeploymentStatus',
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$indexes = [
'_key_deploymentId',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'deployments':
$attributes = [
'buildCommands',
'sourcePath',
'buildOutput',
'adapter',
'fallbackFile',
'sourceSize',
'sourceMetadata',
'sourceChunksTotal',
'sourceChunksUploaded',
'screenshotLight',
'screenshotDark',
'buildStartedAt',
'buildEndedAt',
'buildDuration',
'buildSize',
'status',
'buildPath',
'buildLogs',
'totalSize',
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$indexes = [
'_key_sourceSize',
'_key_buildSize',
'_key_totalSize',
'_key_buildDuration',
'_key_type',
'_key_status',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'executions':
$attributes = [
'resourceInternalId',
'resourceId',
'resourceType'
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$indexes = [
'_key_resource',
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'variables':
$attributes = [
'secret',
];
try {
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
} catch (\Throwable $th) {
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
}
$this->dbForProject->purgeCachedCollection($id);
break;
default:
break;
}
}
}
/**
* Fix run on each document
*
* @param Document $document
* @return Document
* @throws Conflict
* @throws Structure
* @throws Timeout
* @throws \Utopia\Database\Exception
* @throws \Utopia\Database\Exception\Authorization
* @throws \Utopia\Database\Exception\Query
*/
private function migrateDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'rules':
/*
1. Convert "resourceType" to "type". Convert "function" to "deployment"
2. Convert "resourceId" to "deploymentResourceId"
3. Convert "resourceInternalId" to "deploymentResourceInternalId"
4. Fill "trigger" with "manual"
5. Fill "deploymentResourceType". If "resourceType" is "function", set "deploymentResourceType" to "function"
6. Fill "search" with "{$id} {domain}"
7. Set "region" to project region
8. Fill "owner" with "Appwrite" if "domain" ends with "functions" or "sites"
9. Fill "deploymentId" and "deploymentInternalId". If "deploymentResourceType" is "function", get project DB, and find function with ID "resourceId". Then fill rule's "deploymentId" with function's "deployment", and "deploymentId" as backup
*/
$deploymentResourceType = null;
$type = $document->getAttribute('resourceType', $document->getAttribute('type', ''));
if ($type === 'function') {
$type = 'deployment';
$deploymentResourceType = 'function';
}
$resourceId = $document->getAttribute('resourceId', $document->getAttribute('deploymentResourceId'));
$resourceInternalId = $document->getAttribute('resourceInternalId', $document->getAttribute('deploymentResourceInternalId'));
$document
->setAttribute('type', $type)
->setAttribute('trigger', 'manual')
->setAttribute('deploymentResourceId', $resourceId)
->setAttribute('deploymentResourceInternalId', $resourceInternalId)
->setAttribute('deploymentResourceType', $document->getAttribute('deploymentResourceType', $deploymentResourceType))
->setAttribute('search', \implode(' ', [$document->getId(), $document->getAttribute('domain', '')]));
$project = $this->dbForProject->getDocument('projects', $document->getAttribute('projectId'));
if ($project->isEmpty()) {
Console::warning("Project \"{$document->getAttribute('projectId')}\" not found for rule \"{$document->getId()}\"");
$document->setAttribute('region', System::getEnv('_APP_REGION', 'default'));
break;
}
$document->setAttribute('region', $project->getAttribute('region', System::getEnv('_APP_REGION', 'default')));
$domain = $document->getAttribute('domain', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$owner = $document->getAttribute('owner', '');
if (
empty($owner) &&
(!empty($functionsDomain) && \str_ends_with($domain, $functionsDomain)) ||
(!empty($sitesDomain) && \str_ends_with($domain, $sitesDomain))
) {
$document->setAttribute('owner', 'Appwrite');
}
if ($deploymentResourceType === 'function') {
$dbForOwnerProject = ($this->getProjectDB)($project);
$function = $dbForOwnerProject->getDocument('functions', $resourceId);
if ($function->isEmpty()) {
Console::warning("Function \"{$resourceId}\" not found for rule \"{$document->getId()}\"");
break;
}
$deploymentId = $function->getAttribute('deployment', $function->getAttribute('deploymentId', $document->getAttribute('deploymentId', '')));
$deploymentInternalId = $function->getAttribute('deploymentInternalId', $document->getAttribute('deploymentInternalId', ''));
$document
->setAttribute('deploymentId', $deploymentId)
->setAttribute('deploymentInternalId', $deploymentInternalId);
}
break;
case 'variables':
/*
1. Fill "secret" with "false"
*/
$document->setAttribute('secret', $document->getAttribute('secret', false));
break;
case 'executions':
/*
1. Convert "functionInternalId" to "resourceInternalId"
2. Convert "functionId" to "resourceId"
3. Fill "resourceType" with "functions"
*/
$document
->setAttribute('resourceInternalId', $document->getAttribute('functionInternalId', $document->getAttribute('resourceInternalId')))
->setAttribute('resourceId', $document->getAttribute('functionId', $document->getAttribute('resourceId', '')))
->setAttribute('resourceType', $document->getAttribute('resourceType', 'functions'));
break;
case 'functions':
/*
1. Convert "deployment" to "deploymentId"
--- Fetch activeDeployment from "deploymentId"
2. Fill "deploymentCreatedAt" with deployment's "$createdAt"
--- Fetch latestDeployment using find()
3. Fill latestDeploymentId with latestDeployment's "$id"
4. Fill latestDeploymentInternalId with latestDeployment's "$internalId"
5. Fill latestDeploymentCreatedAt with latestDeployment's "$createdAt"
6. Fill latestDeploymentStatus with latestDeployment's build's "status"
*/
if (empty($document->getAttribute('deployment'))) {
break;
}
$document->setAttribute('deploymentId', $document->getAttribute('deployment', $document->getAttribute('deploymentId', '')));
$deploymentId = $document->getAttribute('deploymentId');
$deployment = $this->dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
Console::warning("Deployment \"{$deploymentId}\" not found for function \"{$document->getId()}\"");
break;
}
$document->setAttribute('deploymentCreatedAt', $deployment->getCreatedAt());
$latestDeployment = $this->dbForProject->findOne('deployments', [
Query::orderDesc(),
Query::equal('resourceId', [$document->getId()]),
Query::equal('resourceType', ['functions']),
]);
if ($latestDeployment->isEmpty()) {
Console::warning("Latest deployment not found for function \"{$document->getId()}\"");
break;
}
$latestBuild = $this->dbForProject->getDocument('builds', $latestDeployment->getAttribute('buildId', ''));
if ($latestBuild->isEmpty()) {
Console::warning("Build \"{$latestDeployment->getAttribute('buildId')}\" not found for deployment \"{$latestDeployment->getId()}\"");
break;
}
$document
->setAttribute('latestDeploymentId', $latestDeployment->getId())
->setAttribute('latestDeploymentInternalId', $latestDeployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $latestBuild->getAttribute('status', $document->getAttribute('latestDeploymentStatus', '')));
break;
case 'deployments':
/*
6. Convert "commands" to "buildCommands"
7. Convert "path" to "sourcePath"
8. Convert "size" to "sourceSize"
9. Convert "metadata" to "sourceMetadata"
10. Convert "chunksTotal" to "sourceChunksTotal"
11. Convert "chunksUploaded" to "sourceChunksUploaded"
--- Get build of deployment
12. Convert build's "startTime" to "buildStartedAt"
13. Convert build's "endTime" to "buildEndedAt"
14. Convert build's "duration" to "buildDuration"
15. Convert build's "size" to "buildSize"
16. Convert build's "status" to "status"
17. Convert build's "path" to "buildPath"
18. Convert build's "logs" to "buildLogs"
19. Fill "totalSize" with "buildSize" plus "sourceSize"
*/
$document
->setAttribute('buildCommands', $document->getAttribute('commands', $document->getAttribute('buildCommands', '')))
->setAttribute('sourcePath', $document->getAttribute('path', $document->getAttribute('sourcePath', '')))
->setAttribute('sourceSize', $document->getAttribute('size', $document->getAttribute('sourceSize', 0)))
->setAttribute('sourceMetadata', $document->getAttribute('metadata', $document->getAttribute('sourceMetadata', [])))
->setAttribute('sourceChunksTotal', $document->getAttribute('chunksTotal', $document->getAttribute('sourceChunksTotal', 0)))
->setAttribute('sourceChunksUploaded', $document->getAttribute('chunksUploaded', $document->getAttribute('sourceChunksUploaded', 0)));
$build = new Document();
if (!empty($document->getAttribute('buildId'))) {
$build = $this->dbForProject->getDocument('builds', $document->getAttribute('buildId'));
}
$document
->setAttribute('buildStartedAt', $build->getAttribute('startTime', $document->getAttribute('buildStartTime', '')))
->setAttribute('buildEndedAt', $build->getAttribute('endTime', $document->getAttribute('buildEndTime', '')))
->setAttribute('buildDuration', $build->getAttribute('duration', $document->getAttribute('buildDuration', 0)))
->setAttribute('buildSize', $build->getAttribute('size', $document->getAttribute('buildSize', 0)))
->setAttribute('status', $build->getAttribute('status', $document->getAttribute('status', '')))
->setAttribute('buildPath', $build->getAttribute('path', $document->getAttribute('buildPath', '')))
->setAttribute('buildLogs', $build->getAttribute('logs', $document->getAttribute('buildLogs', '')));
$totalSize = $document->getAttribute('buildSize', 0)
+ $document->getAttribute('sourceSize', 0);
$document->setAttribute('totalSize', $totalSize);
break;
case 'migrations':
/*
1. Fill "options" with "[]"
*/
$document->setAttribute('options', $document->getAttribute('options', []));
break;
default:
break;
}
return $document;
}
private function cleanCollections(): void
{
$projectInternalId = $this->project->getInternalId();
$collectionType = match ($projectInternalId) {
'console' => 'console',
default => 'projects',
};
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
Console::log("Cleaning up collection \"{$id}\"");
switch ($id) {
case '_metadata':
if (!$this->dbForProject->getCollection('builds')->isEmpty()) {
$this->dbForProject->deleteCollection('builds');
}
break;
case 'rules':
$attributes = [
'resourceId',
'resourceInternalId',
'resourceType',
];
foreach ($attributes as $attribute) {
try {
$this->dbForProject->deleteAttribute($id, $attribute);
} catch (\Throwable $th) {
Console::warning("Failed to delete attribute \"$attribute\" from collection {$id}: {$th->getMessage()}");
}
}
$indexesToDelete = [
'_key_resourceId',
'_key_resourceInternalId',
'_key_resourceType',
];
foreach ($indexesToDelete as $index) {
try {
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to delete index \"$index\" from collection {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'functions':
try {
$this->dbForProject->deleteAttribute($id, 'deployment');
} catch (\Throwable $th) {
Console::warning("Failed to delete attribute \"deployment\" from collection {$id}: {$th->getMessage()}");
}
$indexesToDelete = [
'_key_deployment'
];
foreach ($indexesToDelete as $index) {
try {
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to delete index \"$index\" from collection {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'deployments':
$attributes = [
'buildInternalId',
'buildId',
'commands',
'path',
'size',
'metadata',
'chunksTotal',
'chunksUploaded',
'search'
];
foreach ($attributes as $attribute) {
try {
$this->dbForProject->deleteAttribute($id, $attribute);
} catch (\Throwable $th) {
Console::warning("Failed to delete attribute \"$attribute\" from collection {$id}: {$th->getMessage()}");
}
}
$indexesToDelete = [
'_key_buildId',
'_key_size',
'_key_search'
];
foreach ($indexesToDelete as $index) {
try {
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to delete index \"$index\" from collection {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'executions':
$attributes = [
'functionId',
'functionInternalId',
'search'
];
foreach ($attributes as $attribute) {
try {
$this->dbForProject->deleteAttribute($id, $attribute);
} catch (\Throwable $th) {
Console::warning("Failed to delete attribute \"$attribute\" from collection {$id}: {$th->getMessage()}");
}
}
$indexesToDelete = [
'_key_function',
'_fulltext_search'
];
foreach ($indexesToDelete as $index) {
try {
$this->dbForProject->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("Failed to delete index \"$index\" from collection {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
break;
default:
break;
}
}
}
}
@@ -4,24 +4,22 @@ namespace Appwrite\Network\Validator;
use Utopia\Validator;
class CNAME extends Validator
class DNS extends Validator
{
public const RECORD_A = 'a';
public const RECORD_AAAA = 'aaaa';
public const RECORD_CNAME = 'cname';
/**
* @var mixed
*/
protected mixed $logs;
/**
* @var string
*/
protected $target;
/**
* @param string $target
*/
public function __construct($target)
public function __construct(protected string $target, protected string $type = self::RECORD_CNAME)
{
$this->target = $target;
}
/**
@@ -29,7 +27,7 @@ class CNAME extends Validator
*/
public function getDescription(): string
{
return 'Invalid CNAME record';
return 'Invalid DNS record';
}
/**
@@ -41,20 +39,34 @@ class CNAME extends Validator
}
/**
* Check if CNAME record target value matches selected target
* Check if DNS record value matches specific value
*
* @param mixed $domain
*
* @return bool
*/
public function isValid($domain): bool
public function isValid($value): bool
{
if (!is_string($domain)) {
$typeNative = match ($this->type) {
self::RECORD_A => DNS_A,
self::RECORD_AAAA => DNS_AAAA,
self::RECORD_CNAME => DNS_CNAME,
default => throw new \Exception('Record type not supported.')
};
$dnsKey = match ($this->type) {
self::RECORD_A => 'ip',
self::RECORD_AAAA => 'ipv6',
self::RECORD_CNAME => 'target',
default => throw new \Exception('Record type not supported.')
};
if (!is_string($value)) {
return false;
}
try {
$records = \dns_get_record($domain, DNS_CNAME);
$records = \dns_get_record($value, $typeNative);
$this->logs = $records;
} catch (\Throwable $th) {
return false;
@@ -65,7 +77,7 @@ class CNAME extends Validator
}
foreach ($records as $record) {
if (isset($record['target']) && $record['target'] === $this->target) {
if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) {
return true;
}
}
+12
View File
@@ -2,7 +2,13 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
use Appwrite\Platform\Modules\Tokens;
use Utopia\Platform\Platform;
class Appwrite extends Platform
@@ -10,5 +16,11 @@ class Appwrite extends Platform
public function __construct()
{
parent::__construct(new Core());
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
$this->addModule(new Tokens\Module());
}
}
@@ -0,0 +1,272 @@
<?php
namespace Appwrite\Platform\Modules\Compute;
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\Query;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class Base extends Action
{
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$entrypoint = $function->getAttribute('entrypoint', '');
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
$providerRepositoryId = $function->getAttribute('providerRepositoryId', '');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$commitDetails = [];
$branchUrl = "";
$providerBranch = "";
// TODO: Support tag in future
if ($referenceType === 'branch') {
$providerBranch = empty($reference) ? $function->getAttribute('providerBranch', 'main') : $reference;
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
try {
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} elseif ($referenceType === 'commit') {
try {
$commitDetails = $github->getCommit($owner, $repositoryName, $reference);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $function->getAttribute('commands', ''),
'type' => 'vcs',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $function->getAttribute('repositoryId', ''),
'repositoryInternalId' => $function->getAttribute('repositoryInternalId', ''),
'providerBranchUrl' => $branchUrl,
'providerRepositoryName' => $repositoryName,
'providerRepositoryOwner' => $owner,
'providerRepositoryUrl' => $repositoryUrl,
'providerCommitHash' => $commitDetails['commitHash'] ?? '',
'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '',
'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '',
'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'),
'providerCommitUrl' => $commitDetails['commitUrl'] ?? '',
'providerBranch' => $providerBranch,
'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''),
'activate' => $activate,
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment)
->setTemplate($template);
return $deployment;
}
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
$providerRepositoryId = $site->getAttribute('providerRepositoryId', '');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$commitDetails = [];
$branchUrl = "";
$providerBranch = "";
// TODO: Support tag in future
if ($referenceType === 'branch') {
$providerBranch = empty($reference) ? $site->getAttribute('providerBranch', 'main') : $reference;
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
try {
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} elseif ($referenceType === 'commit') {
try {
$commitDetails = $github->getCommit($owner, $repositoryName, $reference);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
$commands = [];
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'sites',
'buildCommands' => implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'type' => 'vcs',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $site->getAttribute('repositoryId', ''),
'repositoryInternalId' => $site->getAttribute('repositoryInternalId', ''),
'providerBranchUrl' => $branchUrl,
'providerRepositoryName' => $repositoryName,
'providerRepositoryOwner' => $owner,
'providerRepositoryUrl' => $repositoryUrl,
'providerCommitHash' => $commitDetails['commitHash'] ?? '',
'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '',
'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '',
'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'),
'providerCommitUrl' => $commitDetails['commitUrl'] ?? '',
'providerBranch' => $providerBranch,
'providerRootDirectory' => $site->getAttribute('providerRootDirectory', ''),
'activate' => $activate,
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'trigger' => 'deployment',
'type' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getInternalId(),
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment)
->setTemplate($template);
return $deployment;
}
protected function listRules(Document $project, array $queries, Database $database, callable $callback): void
{
$limit = 100;
$cursor = null;
do {
$queries = \array_merge([
Query::limit($limit),
Query::equal("projectInternalId", [$project->getInternalId()])
], $queries);
if ($cursor !== null) {
$queries[] = Query::cursorAfter($cursor);
}
$results = $database->find('rules', $queries);
$total = \count($results);
if ($total > 0) {
$cursor = $results[$total - 1];
}
if ($total < $limit) {
$cursor = null;
}
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
} while (!\is_null($cursor));
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Functions;
namespace Appwrite\Platform\Modules\Compute;
class Specification
{
@@ -1,10 +1,10 @@
<?php
namespace Appwrite\Functions\Validator;
namespace Appwrite\Platform\Modules\Compute\Validator;
use Utopia\Validator;
class RuntimeSpecification extends Validator
class Specification extends Validator
{
private array $plan;
@@ -0,0 +1,90 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Resources;
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\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Domain;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getResource';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/resources')
->desc('Check resource ID availability')
->groups(['api', 'projects'])
->label('scope', 'rules.read')
->label('sdk', new Method(
namespace: 'console',
group: null,
name: 'getResource',
description: <<<EOT
Check if a resource ID is available.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE,
))
->label('abuse-limit', 120)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('value', '', new Text(256), 'Resource value.')
->param('type', '', new WhiteList(['rules']), 'Resource type.')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $value,
string $type,
Response $response,
Database $dbForPlatform
) {
if ($type === 'rules') {
$validator = new Domain($value);
if (!$validator->isValid($value)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $validator->getDescription());
}
$document = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal('domain', [$value]),
]));
if (!$document->isEmpty()) {
throw new Exception(Exception::RESOURCE_ALREADY_EXISTS);
}
$response->noContent();
}
// Only occurs if type is added into whitelist, but not supported in action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid type');
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Console\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,16 @@
<?php
namespace Appwrite\Platform\Modules\Console\Services;
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Resources
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
}
}
@@ -0,0 +1,313 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments')
->desc('Create deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'deployment.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'createDeployment',
description: <<<EOT
Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.
This endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https://appwrite.io/docs/functions).
Use the "command" param to set the entrypoint used to execute your code.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
requestType: ContentType::MULTIPART,
type: MethodType::UPLOAD,
packaging: true,
))
->param('functionId', '', new UID(), 'Function ID.')
->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true)
->param('commands', null, new Text(8192, 0), 'Build Commands.', true)
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('project')
->inject('deviceForFunctions')
->inject('deviceForLocal')
->inject('queueForBuilds')
->inject('plan')
->callback([$this, 'action']);
}
public function action(
string $functionId,
?string $entrypoint,
?string $commands,
mixed $code,
mixed $activate,
Request $request,
Response $response,
Database $dbForProject,
Event $queueForEvents,
Document $project,
Device $deviceForFunctions,
Device $deviceForLocal,
Build $queueForBuilds,
array $plan
) {
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if ($entrypoint === null) {
$entrypoint = $function->getAttribute('entrypoint', '');
}
if ($commands === null) {
$commands = $function->getAttribute('commands', '');
}
if (empty($entrypoint)) {
throw new Exception(Exception::FUNCTION_ENTRYPOINT_MISSING);
}
$file = $request->getFiles('code');
// GraphQL multipart spec adds files with index keys
if (empty($file)) {
$file = $request->getFiles(0);
}
if (empty($file)) {
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
}
$functionSizeLimit = (int) System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000');
if (isset($plan['deploymentSize'])) {
$functionSizeLimit = $plan['deploymentSize'] * 1000 * 1000;
}
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
$fileSizeValidator = new FileSize($functionSizeLimit);
$upload = new Upload();
// Make sure we handle a single file and multiple files the same way
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$contentRange = $request->getHeader('content-range');
$deploymentId = ID::unique();
$chunk = 1;
$chunks = 1;
if (!empty($contentRange)) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
// TODO make `end >= $fileSize` in next breaking version
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
}
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
if ($end === $fileSize - 1 || $end === $fileSize) {
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
$chunks = $chunk = -1;
} else {
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
}
}
if (!$fileSizeValidator->isValid($fileSize) && $functionSizeLimit !== 0) { // Check if file size is exceeding allowed limit
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
}
if (!$upload->isValid($fileTmpName)) {
throw new Exception(Exception::STORAGE_INVALID_FILE);
}
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($chunk === -1) {
$chunk = $chunks;
}
}
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
}
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$functionId]),
Query::equal('resourceType', ['functions'])
]);
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
}
}
$fileSize = $deviceForFunctions->getFileSize($path);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
}
// Start the build
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment);
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
}
}
$metadata = null;
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,136 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
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\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Delete deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].delete')
->label('audits.event', 'deployment.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'deleteDeployment',
description: <<<EOT
Delete a code deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
Response $response,
Database $dbForProject,
DeleteEvent $queueForDeletes,
Event $queueForEvents,
Device $deviceForFunctions
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
}
if (!empty($deployment->getAttribute('sourcePath', ''))) {
if (!($deviceForFunctions->delete($deployment->getAttribute('sourcePath', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
if ($function->getAttribute('latestDeploymentId') === $deployment->getId()) {
$latestDeployment = $dbForProject->findOne('deployments', [
Query::equal('resourceType', ['functions']),
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::orderDesc('$createdAt'),
]);
$function = $dbForProject->updateDocument(
'functions',
$function->getId(),
$function
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt())
->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getInternalId())
->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId())
->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''))
);
}
if ($function->getAttribute('deploymentId') === $deployment->getId()) { // Reset function deployment
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'deploymentId' => '',
'deploymentInternalId' => '',
'deploymentCreatedAt' => '',
])));
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($deployment);
$response->noContent();
}
}
@@ -0,0 +1,152 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Download;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeploymentDownload';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', ['type' => 'output'])
->groups(['api', 'functions'])
->desc('Get deployment download')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'getDeploymentDownload',
description: <<<EOT
Get a function deployment content by its unique ID. The endpoint response return with a 'Content-Disposition: attachment' header that tells the browser to start downloading the file to user downloads directory.
EOT,
auth: [AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::ANY,
type: MethodType::LOCATION
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true)
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
string $type,
Response $response,
Request $request,
Database $dbForProject,
Device $deviceForFunctions,
Device $deviceForBuilds
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForFunctions;
break;
}
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response
->setContentType('application/gzip')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
$start = $request->getRangeStart();
$end = $request->getRangeEnd();
$unit = $request->getRangeUnit();
if ($end === null) {
$end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1));
}
if ($unit !== 'bytes' || $start >= $end || $end >= $size) {
throw new Exception(Exception::STORAGE_INVALID_RANGE);
}
$response
->addHeader('Accept-Ranges', 'bytes')
->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size)
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($device->read($path, $start, ($end - $start + 1)));
}
if ($size > APP_STORAGE_READ_BUFFER) {
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
$response->chunk(
$device->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
),
(($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size
);
}
} else {
$response->send($device->read($path));
}
}
}
@@ -0,0 +1,136 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Duplicate;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDuplicateDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments/duplicate')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc('Create duplicate deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'createDuplicateDeployment',
description: <<<EOT
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
string $buildId,
Response $response,
Database $dbForProject,
Event $queueForEvents,
Build $queueForBuilds,
Device $deviceForFunctions
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('sourcePath');
if (empty($path) || !$deviceForFunctions->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'sourcePath' => $destination,
'totalSize' => $deployment->getAttribute('sourceSize', 0),
'entrypoint' => $function->getAttribute('entrypoint'),
'buildCommands' => $function->getAttribute('commands', ''),
'buildStartedAt' => null,
'buildEndedAt' => null,
'buildDuration' => null,
'buildSize' => null,
'status' => 'waiting',
'buildPath' => '',
'buildLogs' => '',
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Get deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'getDeployment',
description: <<<EOT
Get a function deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
Response $response,
Database $dbForProject
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,120 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Status;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeploymentStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/status')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build')
->desc('Update deployment status')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'updateDeploymentStatus',
description: <<<EOT
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->inject('executor')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
Response $response,
Database $dbForProject,
Document $project,
Event $queueForEvents,
Executor $executor
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if (\in_array($deployment->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($deployment->getAttribute('buildStartedAt', 'now'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
'buildEndedAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
]));
if ($deployment->getInternalId() === $function->getAttribute('latestDeploymentInternalId', '')) {
$function = $function->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
}
try {
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,174 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Template;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createTemplateDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments/template')
->desc('Create template deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'deployment.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'createTemplateDeployment',
description: <<<EOT
Create a deployment based on a template.
Use this endpoint with combination of [listTemplates](https://appwrite.io/docs/server/functions#listTemplates) to find the template details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
))
->param('functionId', '', new UID(), 'Function ID.')
->param('repository', '', new Text(128, 0), 'Repository name of the template.')
->param('owner', '', new Text(128, 0), 'The name of the owner of the template.')
->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.')
->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('project')
->inject('queueForBuilds')
->inject('gitHub')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $repository,
string $owner,
string $rootDirectory,
string $version,
bool $activate,
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Event $queueForEvents,
Document $project,
Build $queueForBuilds,
GitHub $github
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$template = new Document([
'repositoryName' => $repository,
'ownerName' => $owner,
'rootDirectory' => $rootDirectory,
'version' => $version
]);
if (!empty($function->getAttribute('providerRepositoryId'))) {
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$deployment = $this->redeployVcsFunction(
request: $request,
function: $function,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
activate: $activate
);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
$deploymentId = ID::unique();
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'type' => 'manual',
'activate' => $activate,
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment)
->setTemplate($template);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,124 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createVcsDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments/vcs')
->desc('Create VCS deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
->label('audits.event', 'deployment.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'createVcsDeployment',
description: <<<EOT
Create a deployment when a function is connected to VCS.
This endpoint lets you create deployment from a branch, commit, or a tag.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
))
->param('functionId', '', new UID(), 'Function ID.')
// TODO: Support tag in future
->param('type', '', new WhiteList(['branch', 'commit']), 'Type of reference passed. Allowed values are: branch, commit')
->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name, commit hash')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $type,
string $reference,
bool $activate,
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$deployment = $this->redeployVcsFunction(
request: $request,
function: $function,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
activate: $activate,
reference: $reference,
referenceType: $type
);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,129 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listDeployments';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments')
->desc('List deployments')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'deployments',
name: 'listDeployments',
description: <<<EOT
Get a list of all the function's code deployments. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT_LIST,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $functionId,
array $queries,
string $search,
Response $response,
Database $dbForProject
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Set resource queries
$queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$deploymentId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('deployments', $deploymentId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'deployments' => $results,
'total' => $total,
]), Response::MODEL_DEPLOYMENT_LIST);
}
}
@@ -0,0 +1,477 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/executions')
->desc('Create execution')
->groups(['api', 'functions'])
->label('scope', 'execution.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].executions.[executionId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'executions',
name: 'createExecution',
description: <<<EOT
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_EXECUTION,
)
],
contentType: ContentType::MULTIPART,
))
->param('functionId', '', new UID(), 'Function ID.')
->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true)
->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true)
->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true)
->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true)
->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true)
->inject('response')
->inject('request')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('executor')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $body,
mixed $async,
string $path,
string $method,
mixed $headers,
?string $scheduledAt,
Response $response,
Request $request,
Document $project,
Database $dbForProject,
Database $dbForPlatform,
Document $user,
Event $queueForEvents,
StatsUsage $queueForStatsUsage,
Func $queueForFunctions,
Reader $geodb,
Executor $executor
) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.');
}
/**
* @var array<string, mixed> $headers
*/
$assocParams = ['headers'];
foreach ($assocParams as $assocParam) {
if (!empty('headers') && !is_array($$assocParam)) {
$$assocParam = \json_decode($$assocParam, true);
}
}
$booleanParams = ['async'];
foreach ($booleanParams as $booleamParam) {
if (!empty($$booleamParam) && !is_bool($$booleamParam)) {
$$booleamParam = $$booleamParam === "true" ? true : false;
}
}
// 'headers' validator
$validator = new Headers();
if (!$validator->isValid($headers)) {
throw new Exception($validator->getDescription(), 400);
}
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
if (\is_null($runtime)) {
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', '')));
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
$validator = new Authorization('execute');
if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
}
$jwt = ''; // initialize
if (!$user->isEmpty()) { // If userId exists, generate a JWT for function
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$current = $session;
}
}
if (!$current->isEmpty()) {
$jwtExpiry = $function->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$jwt = $jwtObj->encode([
'userId' => $user->getId(),
'sessionId' => $current->getId(),
]);
}
}
$jwtExpiry = $function->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $function->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
$ip = $headers['x-real-ip'] ?? '';
if (!empty($ip)) {
$record = $geodb->get($ip);
if ($record) {
$eu = Config::getParam('locale-eu');
$headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? '';
$headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? '';
$headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false';
}
}
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
$executionId = ID::unique();
$status = $async ? 'waiting' : 'processing';
if (!is_null($scheduledAt)) {
$status = 'scheduled';
}
$execution = new Document([
'$id' => $executionId,
'$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http',
'status' => $status, // waiting / processing / completed / failed / scheduled
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => '',
'logs' => '',
'duration' => 0.0,
]);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setContext('function', $function);
if ($async) {
if (is_null($scheduledAt)) {
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
->setFunction($function)
->setBody($body)
->setHeaders($headers)
->setPath($path)
->setMethod($method)
->setJWT($jwt)
->setProject($project)
->setUser($user)
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->trigger();
} else {
$data = [
'headers' => $headers,
'path' => $path,
'method' => $method,
'body' => $body,
'userId' => $user->getId()
];
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => $project->getAttribute('region'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
'resourceInternalId' => $execution->getInternalId(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $scheduledAt,
'data' => $data,
'active' => true,
]));
$execution = $execution
->setAttribute('scheduleId', $schedule->getId())
->setAttribute('scheduleInternalId', $schedule->getInternalId())
->setAttribute('scheduledAt', $scheduledAt);
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
}
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
$durationStart = \microtime(true);
$vars = [];
// V2 vars
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
}
// Shared vars
foreach ($function->getAttribute('varsProject', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
// Function vars
foreach ($function->getAttribute('vars', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_ID' => $functionId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''),
'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''),
'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''),
'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''),
'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''),
'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''),
'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''),
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
]);
/** Execute function */
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $deployment->getAttribute('buildPath', ''),
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
$headersFiltered = [];
foreach ($executionResponse['headers'] as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
$execution->setAttribute('status', $status);
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
$execution->setAttribute('responseHeaders', $headersFiltered);
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
$execution->setAttribute('duration', $executionResponse['duration']);
} catch (\Throwable $th) {
$durationEnd = \microtime(true);
$execution
->setAttribute('duration', $durationEnd - $durationStart)
->setAttribute('status', 'failed')
->setAttribute('responseStatusCode', 500)
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
Console::error($th->getMessage());
if ($th instanceof AppwriteException) {
throw $th;
}
} finally {
$queueForStatsUsage
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
;
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$headers = [];
foreach (($executionResponse['headers'] ?? []) as $key => $value) {
$headers[] = ['name' => $key, 'value' => $value];
}
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$acceptTypes = \explode(', ', $request->getHeader('accept'));
foreach ($acceptTypes as $acceptType) {
if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) {
$response->setContentType(Response::CONTENT_TYPE_JSON);
break;
} elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) {
$response->setContentType(Response::CONTENT_TYPE_MULTIPART);
break;
}
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
}
@@ -0,0 +1,123 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Tasks\ScheduleExecutions;
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\DateTime;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
->desc('Delete execution')
->groups(['api', 'functions'])
->label('scope', 'execution.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].executions.[executionId].delete')
->label('audits.event', 'executions.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'executions',
name: 'deleteExecution',
description: <<<EOT
Delete a function execution by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $executionId,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Event $queueForEvents
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$execution = $dbForProject->getDocument('executions', $executionId);
if ($execution->isEmpty()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$status = $execution->getAttribute('status');
if (!in_array($status, ['completed', 'failed', 'scheduled'])) {
throw new Exception(Exception::EXECUTION_IN_PROGRESS);
}
if (!$dbForProject->deleteDocument('executions', $execution->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB');
}
if ($status === 'scheduled') {
$schedule = $dbForPlatform->findOne('schedules', [
Query::equal('resourceId', [$execution->getId()]),
Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]),
Query::equal('active', [true]),
]);
if (!$schedule->isEmpty()) {
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setPayload($response->output($execution, Response::MODEL_EXECUTION));
$response->noContent();
}
}
@@ -0,0 +1,85 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
->desc('Get execution')
->groups(['api', 'functions'])
->label('scope', 'execution.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'executions',
name: 'getExecution',
description: <<<EOT
Get a function execution log by its unique ID.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $executionId,
Response $response,
Database $dbForProject
) {
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$execution = $dbForProject->getDocument('executions', $executionId);
if ($execution->getAttribute('resourceType') !== 'functions' || $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
if ($execution->isEmpty()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$response->dynamic($execution, Response::MODEL_EXECUTION);
}
}
@@ -0,0 +1,128 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listExecutions';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/executions')
->desc('List executions')
->groups(['api', 'functions'])
->label('scope', 'execution.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'executions',
name: 'listExecutions',
description: <<<EOT
Get a list of all the current user function execution logs. You can use the query params to filter your results.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION_LIST,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Executions(), '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(', ', Executions::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $functionId,
array $queries,
Response $response,
Database $dbForProject
) {
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Set internal queries
$queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$executionId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('executions', $executionId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
]), Response::MODEL_EXECUTION_LIST);
}
}
@@ -0,0 +1,417 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Utopia\Abuse\Abuse;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Roles;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Request;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createFunction';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions')
->desc('Create function')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'function.create')
->label('audits.resource', 'function/{response.$id}')
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'create',
description: <<<EOT
Create a new function. You can pass a list of [permissions](https://appwrite.io/docs/permissions) to allow different project users or team with access to execute the function using the client API.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_FUNCTION,
)
],
))
->param('functionId', '', new CustomId(), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true)
->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true)
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true, deprecated: true)
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true, deprecated: true)
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true, deprecated: true)
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true, deprecated: true)
->inject('response')
->inject('dbForProject')
->inject('timelimit')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('queueForRealtime')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('dbForPlatform')
->inject('request')
->inject('gitHub')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $name,
string $runtime,
array $execute,
array $events,
string $schedule,
int $timeout,
bool $enabled,
bool $logging,
string $entrypoint,
string $commands,
array $scopes,
string $installationId,
string $providerRepositoryId,
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $templateRepository,
string $templateOwner,
string $templateRootDirectory,
string $templateVersion,
Response $response,
Database $dbForProject,
callable $timelimit,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
Realtime $queueForRealtime,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Database $dbForPlatform,
Request $request,
GitHub $github
) {
// Temporary abuse check
$abuseCheck = function () use ($project, $timelimit, $response) {
$abuseKey = "projectId:{projectId},url:{url}";
$abuseLimit = System::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseTime = 86400; // 1 day
$timeLimit = $timelimit($abuseKey, $abuseLimit, $abuseTime);
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{url}', '/v1/functions');
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $abuseTime;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if ($enabled && $abuse->check()) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
};
$abuseCheck();
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported');
}
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
}
$function = $dbForProject->createDocument('functions', new Document([
'$id' => $functionId,
'execute' => $execute,
'enabled' => $enabled,
'live' => true,
'logging' => $logging,
'name' => $name,
'runtime' => $runtime,
'deploymentInternalId' => '',
'deploymentId' => '',
'events' => $events,
'schedule' => $schedule,
'scheduleInternalId' => '',
'scheduleId' => '',
'timeout' => $timeout,
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'search' => implode(' ', [$functionId, $name, $runtime]),
'version' => 'v5',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => '',
'repositoryInternalId' => '',
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification
]));
$schedule = Authorization::skip(
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => $project->getAttribute('region'),
'resourceType' => 'function',
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $function->getAttribute('schedule'),
'active' => false,
]))
);
$function->setAttribute('scheduleId', $schedule->getId());
$function->setAttribute('scheduleInternalId', $schedule->getInternalId());
// Git connect logic
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'function',
'providerPullRequestIds' => []
]));
$function->setAttribute('repositoryId', $repository->getId());
$function->setAttribute('repositoryInternalId', $repository->getInternalId());
}
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
// Backwards compatibility with 1.6 behaviour
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($requestFormat && version_compare($requestFormat, '1.7.0', '<')) {
// build from template
$template = new Document([]);
if (
!empty($templateRepository)
&& !empty($templateOwner)
&& !empty($templateRootDirectory)
&& !empty($templateVersion)
) {
$template->setAttribute('repositoryName', $templateRepository)
->setAttribute('ownerName', $templateOwner)
->setAttribute('rootDirectory', $templateRootDirectory)
->setAttribute('version', $templateVersion);
}
if (!empty($providerRepositoryId)) {
// Deploy VCS
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$deployment = $this->redeployVcsFunction(
request: $request,
function: $function,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
activate: true,
reference: $providerBranch,
referenceType: 'branch'
);
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
} elseif (!$template->isEmpty()) {
// Deploy non-VCS from template
$deploymentId = ID::unique();
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'type' => 'manual',
'activate' => true,
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment)
->setTemplate($template);
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'status' => 'verified',
'type' => 'deployment',
'trigger' => 'manual',
'deploymentId' => !isset($deployment) || $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => !isset($deployment) || $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'function',
'deploymentResourceId' => $function->getId(),
'deploymentResourceInternalId' => $function->getInternalId(),
'deploymentVcsProviderBranch' => '',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
$ruleModel = new Rule();
$ruleCreate =
$queueForEvents
->setProject($project)
->setEvent('rules.[ruleId].create')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())));
/** Trigger Webhook */
$queueForWebhooks
->from($ruleCreate)
->trigger();
/** Trigger Functions */
$queueForFunctions
->from($ruleCreate)
->trigger();
/** Trigger Realtime Events */
$queueForRealtime
->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
->trigger();
}
}
$queueForEvents->setParam('functionId', $function->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,100 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteFunction';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId')
->desc('Delete function')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].delete')
->label('audits.event', 'function.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'delete',
description: <<<EOT
Delete a function by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $functionId,
Response $response,
Database $dbForProject,
DeleteEvent $queueForDeletes,
Event $queueForEvents,
Database $dbForPlatform
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('functions', $function->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB');
}
// Inform scheduler to no longer run function
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($function);
$queueForEvents->setParam('functionId', $function->getId());
$response->noContent();
}
}
@@ -0,0 +1,134 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions\Deployment;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Query;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateFunctionDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/functions/:functionId/deployment')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Update function\'s deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'updateFunctionDeployment',
description: <<<EOT
Update the function active deployment. Use this endpoint to switch the code deployment that should be used when visitor opens your function.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $deploymentId,
Document $project,
Response $response,
Database $dbForProject,
Event $queueForEvents,
Database $dbForPlatform
) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
$oldDeploymentInternalId = $function->getAttribute('deploymentInternalId', '');
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'deploymentCreatedAt' => $deployment->getCreatedAt(),
])));
// Inform scheduler if function is still active
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queries = [
Query::equal('trigger', 'manual'),
Query::equal("type", ["deployment"]),
Query::equal("deploymentResourceType", ["function"]),
Query::equal("deploymentResourceInternalId", [$function->getInternalId()]),
];
if (empty($oldDeploymentInternalId)) {
$queries[] = Query::equal("deploymentInternalId", [""]);
} else {
$queries[] = Query::equal("deploymentInternalId", [$oldDeploymentInternalId]);
}
$this->listRules($project, $queries, $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getInternalId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,68 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getFunction';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId')
->desc('Get function')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'get',
description: <<<EOT
Get a function by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $functionId,
Response $response,
Database $dbForProject
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$response->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,291 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Roles;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateFunction';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/functions/:functionId')
->desc('Update function')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].update')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'function.update')
->label('audits.resource', 'function/{response.$id}')
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'update',
description: <<<EOT
Update function by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true)
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true)
->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true)
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true)
->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('executor')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $name,
string $runtime,
array $execute,
array $events,
string $schedule,
int $timeout,
bool $enabled,
bool $logging,
string $entrypoint,
string $commands,
array $scopes,
string $installationId,
?string $providerRepositoryId,
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
Request $request,
Response $response,
Database $dbForProject,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
Database $dbForPlatform,
GitHub $github,
Executor $executor
) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
}
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if (empty($runtime)) {
$runtime = $function->getAttribute('runtime');
}
$enabled ??= $function->getAttribute('enabled', true);
$repositoryId = $function->getAttribute('repositoryId', '');
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
if (empty($entrypoint)) {
$entrypoint = $function->getAttribute('entrypoint', '');
}
$isConnected = !empty($function->getAttribute('providerRepositoryId', ''));
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', ['function']),
Query::limit(100),
]);
foreach ($repositories as $repository) {
$dbForPlatform->deleteDocument('repositories', $repository->getId());
}
$providerRepositoryId = '';
$installationId = '';
$providerBranch = '';
$providerRootDirectory = '';
$providerSilentMode = true;
$repositoryId = '';
$repositoryInternalId = '';
}
// Git connect logic
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'function',
'providerPullRequestIds' => []
]));
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getInternalId();
}
$live = true;
if (
$function->getAttribute('name') !== $name ||
$function->getAttribute('entrypoint') !== $entrypoint ||
$function->getAttribute('commands') !== $commands ||
$function->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
$function->getAttribute('runtime') !== $runtime
) {
$live = false;
}
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'execute' => $execute,
'name' => $name,
'runtime' => $runtime,
'events' => $events,
'schedule' => $schedule,
'timeout' => $timeout,
'enabled' => $enabled,
'live' => $live,
'logging' => $logging,
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $repositoryId,
'repositoryInternalId' => $repositoryInternalId,
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'search' => implode(' ', [$functionId, $name, $runtime]),
])));
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
$this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
}
// Inform scheduler if function is still active
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents->setParam('functionId', $function->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,117 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Functions;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listFunctions';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions')
->groups(['api', 'functions'])
->desc('List functions')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'functions',
name: 'list',
description: <<<EOT
Get a list of all the project's functions. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION_LIST,
)
]
))
->param('queries', [], new Functions(), '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(', ', Functions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
array $queries,
string $search,
Response $response,
Database $dbForProject
) {
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);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$functionId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('functions', $functionId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Function '{$functionId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$functions = $dbForProject->find('functions', $queries);
$total = $dbForProject->count('functions', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'functions' => $functions,
'total' => $total,
]), Response::MODEL_FUNCTION_LIST);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Runtimes;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listRuntimes';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/runtimes')
->groups(['api'])
->desc('List runtimes')
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'runtimes',
name: 'listRuntimes',
description: <<<EOT
Get a list of all runtimes that are currently active on your instance.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_RUNTIME_LIST,
)
]
))
->inject('response')
->callback([$this, 'action']);
}
public function action(Response $response)
{
$runtimes = Config::getParam('runtimes');
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
$allowed = [];
foreach ($runtimes as $id => $runtime) {
if (!empty($allowList) && !\in_array($id, $allowList)) {
continue;
}
$runtime['$id'] = $id;
$allowed[] = $runtime;
}
$response->dynamic(new Document([
'total' => count($allowed),
'runtimes' => $allowed
]), Response::MODEL_RUNTIME_LIST);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Specifications;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listSpecifications';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/specifications')
->groups(['api', 'functions'])
->desc('List specifications')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'runtimes',
name: 'listSpecifications',
description: <<<EOT
List allowed function specifications for this instance.
EOT,
auth: [AuthType::KEY, AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SPECIFICATION_LIST,
)
]
))
->inject('response')
->inject('plan')
->callback([$this, 'action']);
}
public function action(Response $response, array $plan)
{
$allSpecs = Config::getParam('specifications', []);
$specs = [];
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
// Treat 0 as no limit
if ((empty($maxCpus) || $spec['cpus'] <= $maxCpus) && (empty($maxMemory) || $spec['memory'] <= $maxMemory)) {
$specs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $specs,
'total' => count($specs)
]), Response::MODEL_SPECIFICATION_LIST);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Templates;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getTemplate';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/templates/:templateId')
->desc('Get function template')
->groups(['api', 'functions'])
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'templates',
name: 'getTemplate',
description: <<<EOT
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_FUNCTION,
)
]
))
->param('templateId', '', new Text(128), 'Template ID.')
->inject('response')
->callback([$this, 'action']);
}
public function action(string $templateId, Response $response)
{
$templates = Config::getParam('templates-function', []);
$filtered = \array_filter($templates, function ($template) use ($templateId) {
return $template['id'] === $templateId;
});
$template = array_shift($filtered);
if (empty($template)) {
throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND);
}
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION);
}
}
@@ -0,0 +1,86 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Templates;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listTemplates';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/templates')
->desc('List templates')
->groups(['api'])
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: 'templates',
name: 'listTemplates',
description: <<<EOT
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_FUNCTION_LIST,
)
]
))
->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true)
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true)
->inject('response')
->callback([$this, 'action']);
}
public function action(array $runtimes, array $usecases, int $limit, int $offset, Response $response)
{
$templates = Config::getParam('templates-function', []);
if (!empty($runtimes)) {
$templates = \array_filter($templates, function ($template) use ($runtimes) {
return \count(\array_intersect($runtimes, \array_column($template['runtimes'], 'name'))) > 0;
});
}
if (!empty($usecases)) {
$templates = \array_filter($templates, function ($template) use ($usecases) {
return \count(\array_intersect($usecases, $template['useCases'])) > 0;
});
}
\usort($templates, function ($a, $b) {
return $b['score'] <=> $a['score'];
});
$total = \count($templates);
$templates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([
'templates' => $templates,
'total' => $total,
]), Response::MODEL_TEMPLATE_FUNCTION_LIST);
}
}
@@ -0,0 +1,159 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/usage')
->desc('Get function usage')
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: null,
name: 'getUsage',
description: <<<EOT
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $range, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_SUCCESS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_FAILED),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$buildsTimeTotal = $usage[$metrics[4]]['total'] ?? 0;
$buildsTotal = $usage[$metrics[2]]['total'] ?? 0;
$response->dynamic(new Document([
'range' => $range,
'deploymentsTotal' => $usage[$metrics[0]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
'buildsTotal' => $usage[$metrics[2]]['total'],
'buildsSuccessTotal' => $usage[$metrics[9]]['total'],
'buildsFailedTotal' => $usage[$metrics[10]]['total'],
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
'buildsTimeTotal' => $usage[$metrics[4]]['total'],
'buildsTimeAverage' => $buildsTotal === 0 ? 0 : (int) ($buildsTimeTotal / $buildsTotal),
'executionsTotal' => $usage[$metrics[5]]['total'],
'executionsTimeTotal' => $usage[$metrics[6]]['total'],
'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'],
'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'deployments' => $usage[$metrics[0]]['data'],
'deploymentsStorage' => $usage[$metrics[1]]['data'],
'builds' => $usage[$metrics[2]]['data'],
'buildsStorage' => $usage[$metrics[3]]['data'],
'buildsTime' => $usage[$metrics[4]]['data'],
'executions' => $usage[$metrics[5]]['data'],
'executionsTime' => $usage[$metrics[6]]['data'],
'buildsMbSeconds' => $usage[$metrics[7]]['data'],
'executionsMbSeconds' => $usage[$metrics[8]]['data'],
'buildsSuccess' => $usage[$metrics[9]]['data'],
'buildsFailed' => $usage[$metrics[10]]['data'],
]), Response::MODEL_USAGE_FUNCTION);
}
}
@@ -0,0 +1,149 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Usage;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'getFunctionsUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/usage')
->desc('Get functions usage')
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
group: null,
name: 'listUsage',
description: <<<EOT
Get usage metrics and statistics for all functions in the project. View statistics including total deployments, builds, logs, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_FUNCTIONS,
)
]
))
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $range, Response $response, Database $dbForProject)
{
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
METRIC_FUNCTIONS,
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_DEPLOYMENTS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_STORAGE),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_COMPUTE),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_EXECUTIONS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_SUCCESS),
str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS_FAILED),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'functionsTotal' => $usage[$metrics[0]]['total'],
'deploymentsTotal' => $usage[$metrics[1]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
'buildsTotal' => $usage[$metrics[3]]['total'],
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
'executionsTotal' => $usage[$metrics[6]]['total'],
'executionsTimeTotal' => $usage[$metrics[7]]['total'],
'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'],
'buildsSuccessTotal' => $usage[$metrics[10]]['total'],
'buildsFailedTotal' => $usage[$metrics[11]]['total'],
'functions' => $usage[$metrics[0]]['data'],
'deployments' => $usage[$metrics[1]]['data'],
'deploymentsStorage' => $usage[$metrics[2]]['data'],
'builds' => $usage[$metrics[3]]['data'],
'buildsStorage' => $usage[$metrics[4]]['data'],
'buildsTime' => $usage[$metrics[5]]['data'],
'executions' => $usage[$metrics[6]]['data'],
'executionsTime' => $usage[$metrics[7]]['data'],
'buildsMbSeconds' => $usage[$metrics[8]]['data'],
'executionsMbSeconds' => $usage[$metrics[9]]['data'],
'buildsSuccess' => $usage[$metrics[10]]['data'],
'buildsFailed' => $usage[$metrics[11]]['data'],
]), Response::MODEL_USAGE_FUNCTIONS);
}
}
@@ -0,0 +1,128 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/variables')
->desc('Create variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'variables',
name: 'createVariable',
description: <<<EOT
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
]
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $key,
string $value,
bool $secret,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variableId = ID::unique();
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'function',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,99 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Delete variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'variables',
name: 'deleteVariable',
description: <<<EOT
Delete a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $variableId,
Response $response,
Database $dbForProject,
Database $dbForPlatform
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
}
}
@@ -0,0 +1,83 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Get variable')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label(
'sdk',
new Method(
namespace: 'functions',
group: 'variables',
name: 'getVariable',
description: <<<EOT
Get a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
],
)
)
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $variableId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if (
$variable === false ||
$variable->isEmpty() ||
$variable->getAttribute('resourceInternalId') !== $function->getInternalId() ||
$variable->getAttribute('resourceType') !== 'function'
) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Update variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
group: 'variables',
name: 'updateVariable',
description: <<<EOT
Update variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $functionId,
string $variableId,
string $key,
?string $value,
?bool $secret,
Response $response,
Database $dbForProject,
Database $dbForPlatform
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable->getAttribute('secret') === true && $secret === false) {
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/variables')
->desc('List variables')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label(
'sdk',
new Method(
namespace: 'functions',
group: 'variables',
name: 'listVariables',
description: <<<EOT
Get a list of all variables of a specific function.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
],
)
)
->param('functionId', '', new UID(), 'Function unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$response->dynamic(new Document([
'variables' => $function->getAttribute('vars', []),
'total' => \count($function->getAttribute('vars', [])),
]), Response::MODEL_VARIABLE_LIST);
}
}
@@ -0,0 +1,16 @@
<?php
namespace Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Functions\Services\Http;
use Appwrite\Platform\Modules\Functions\Services\Workers;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
$this->addService('workers', new Workers());
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Services;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Download\Get as DownloadDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Get as GetDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Status\Update as UpdateDeploymentStatus;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Template\Create as CreateTemplateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs\Create as CreateVcsDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\XList as ListDeployments;
use Appwrite\Platform\Modules\Functions\Http\Executions\Create as CreateExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\Delete as DeleteExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\Get as GetExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\XList as ListExecutions;
use Appwrite\Platform\Modules\Functions\Http\Functions\Create as CreateFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Delete as DeleteFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Deployment\Update as UpdateFunctionDeployment;
use Appwrite\Platform\Modules\Functions\Http\Functions\Get as GetFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Update as UpdateFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\XList as ListFunctions;
use Appwrite\Platform\Modules\Functions\Http\Runtimes\XList as ListRuntimes;
use Appwrite\Platform\Modules\Functions\Http\Specifications\XList as ListSpecifications;
use Appwrite\Platform\Modules\Functions\Http\Templates\Get as GetTemplate;
use Appwrite\Platform\Modules\Functions\Http\Templates\XList as ListTemplates;
use Appwrite\Platform\Modules\Functions\Http\Usage\Get as GetUsage;
use Appwrite\Platform\Modules\Functions\Http\Usage\XList as ListUsage;
use Appwrite\Platform\Modules\Functions\Http\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Get as GetVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Update as UpdateVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\XList as ListVariables;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Functions
$this->addAction(CreateFunction::getName(), new CreateFunction());
$this->addAction(GetFunction::getName(), new GetFunction());
$this->addAction(UpdateFunction::getName(), new UpdateFunction());
$this->addAction(ListFunctions::getName(), new ListFunctions());
$this->addAction(DeleteFunction::getName(), new DeleteFunction());
// Runtimes
$this->addAction(ListRuntimes::getName(), new ListRuntimes());
// Specifications
$this->addAction(ListSpecifications::getName(), new ListSpecifications());
// Deployments
$this->addAction(CreateDeployment::getName(), new CreateDeployment());
$this->addAction(GetDeployment::getName(), new GetDeployment());
$this->addAction(UpdateFunctionDeployment::getName(), new UpdateFunctionDeployment());
$this->addAction(ListDeployments::getName(), new ListDeployments());
$this->addAction(DeleteDeployment::getName(), new DeleteDeployment());
$this->addAction(CreateTemplateDeployment::getName(), new CreateTemplateDeployment());
$this->addAction(CreateVcsDeployment::getName(), new CreateVcsDeployment());
$this->addAction(DownloadDeployment::getName(), new DownloadDeployment());
$this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment());
$this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus());
// Executions
$this->addAction(CreateExecution::getName(), new CreateExecution());
$this->addAction(GetExecution::getName(), new GetExecution());
$this->addAction(ListExecutions::getName(), new ListExecutions());
$this->addAction(DeleteExecution::getName(), new DeleteExecution());
// Usage
$this->addAction(GetUsage::getName(), new GetUsage());
$this->addAction(ListUsage::getName(), new ListUsage());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
// Templates
$this->addAction(GetTemplate::getName(), new GetTemplate());
$this->addAction(ListTemplates::getName(), new ListTemplates());
}
}
@@ -0,0 +1,15 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Services;
use Appwrite\Platform\Modules\Functions\Workers\Builds;
use Utopia\Platform\Service;
class Workers extends Service
{
public function __construct()
{
$this->type = Service::TYPE_WORKER;
$this->addAction(Builds::getName(), new Builds());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,96 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\DevKeys;
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\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDevKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/projects/:projectId/dev-keys')
->desc('Create dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
name: 'createDevKey',
description: <<<EOT
Create a new project dev key. Dev keys are project specific and allow you to bypass rate limits and get better error logging during development. Strictly meant for development purposes only.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_DEV_KEY
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format.', false)
->inject('user')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $projectId, string $name, ?string $expire, Document $user, Response $response, Database $dbForPlatform)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$devKeyId = ID::unique();
$key = new Document([
'$id' => $devKeyId,
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
'secret' => \bin2hex(\random_bytes(128)),
]);
$key = $dbForPlatform->createDocument('devKeys', $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($key, Response::MODEL_DEV_KEY);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\DevKeys;
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\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteDevKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Delete dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
name: 'deleteDevKey',
description: <<<EOT
Delete a project\'s dev key by its unique ID. Once deleted, the key will no longer allow bypassing of rate limits and better logging of errors.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE
)
],
contentType: ContentType::NONE
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $projectId, string $keyId, Response $response, Database $dbForPlatform)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->getDocument('devKeys', $keyId);
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$dbForPlatform->deleteDocument('devKeys', $key->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
}
}
@@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\DevKeys;
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\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDevKey';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Get dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
name: 'getDevKey',
description: <<<EOT
Get a project\'s dev key by its unique ID. Dev keys are project specific and allow you to bypass rate limits and get better error logging during development.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEV_KEY
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $projectId, string $keyId, Response $response, Database $dbForPlatform)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->getDocument('devKeys', $keyId);
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_DEV_KEY);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\DevKeys;
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\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDevKey';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Update dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
name: 'updateDevKey',
description: <<<EOT
Update a project\'s dev key by its unique ID. Use this endpoint to update a project\'s dev key name or expiration time.'
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEV_KEY
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format.')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $projectId, string $keyId, string $name, ?string $expire, Response $response, Database $dbForPlatform)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->getDocument('devKeys', $keyId);
if ($key === false || $key->isEmpty() || $key->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$key
->setAttribute('name', $name)
->setAttribute('expire', $expire);
$dbForPlatform->updateDocument('devKeys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($key, Response::MODEL_DEV_KEY);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\DevKeys;
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\DevKeys;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listDevKeys';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/projects/:projectId/dev-keys')
->desc('List dev keys')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
name: 'listDevKeys',
description: <<<EOT
List all the project\'s dev keys. Dev keys are project specific and allow you to bypass rate limits and get better error logging during development.'
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEV_KEY_LIST
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('queries', [], new DevKeys(), '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(', ', DevKeys::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $projectId, ?array $queries, Response $response, Database $dbForPlatform)
{
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('projectInternalId', [$project->getInternalId()]);
$keys = $dbForPlatform->find('devKeys', $queries);
$response->dynamic(new Document([
'devKeys' => $keys,
'total' => count($keys),
]), Response::MODEL_DEV_KEY_LIST);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Projects\Services\Http;
use Utopia\Platform\Module as Base;
class Module extends Base
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,23 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Services;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Create as CreateDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
$this->addAction(CreateDevKey::getName(), new CreateDevKey());
$this->addAction(UpdateDevKey::getName(), new UpdateDevKey());
$this->addAction(GetDevKey::getName(), new GetDevKey());
$this->addAction(ListDevKeys::getName(), new ListDevKeys());
$this->addAction(DeleteDevKey::getName(), new DeleteDevKey());
}
}
@@ -0,0 +1,188 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createAPIRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/api')
->groups(['api', 'proxy'])
->desc('Create API rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
name: 'createAPIRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite's API on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
{
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$deniedDomains[] = $functionsDomain;
}
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', '');
$denyListDomains = \array_map('trim', explode(',', $denyListDomains));
foreach ($denyListDomains as $denyListDomain) {
if (empty($denyListDomain)) {
continue;
}
$deniedDomains[] = $denyListDomain;
}
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
if (empty($validators)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.');
}
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$owner = '';
if (
($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) ||
($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain))
) {
$owner = 'Appwrite';
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'api',
'trigger' => 'manual',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $project->getAttribute('region')
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,206 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createFunctionRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/function')
->groups(['api', 'proxy'])
->desc('Create function rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
name: 'createFunctionRule',
description: <<<EOT
Create a new proxy rule for executing Appwrite Function on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('functionId', '', new UID(), 'ID of function to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$deniedDomains[] = $functionsDomain;
}
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', '');
$denyListDomains = \array_map('trim', explode(',', $denyListDomains));
foreach ($denyListDomains as $denyListDomain) {
if (empty($denyListDomain)) {
continue;
}
$deniedDomains[] = $denyListDomain;
}
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', ''));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
if (empty($validators)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.');
}
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$owner = '';
if (
($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) ||
($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain))
) {
$owner = 'Appwrite';
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'deployment',
'trigger' => 'manual',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'function',
'deploymentResourceId' => $function->getId(),
'deploymentResourceInternalId' => $function->getInternalId(),
'deploymentVcsProviderBranch' => $branch,
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get(), $branch]),
'owner' => $owner,
'region' => $project->getAttribute('region')
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,194 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRedirectRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/redirect')
->groups(['api', 'proxy'])
->desc('Create Redirect rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
name: 'createRedirectRule',
description: <<<EOT
Create a new proxy rule for to redirect from custom domain to another domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('url', null, new URL(), 'Target URL of redirection')
->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $domain, string $url, int $statusCode, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
{
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$deniedDomains[] = $functionsDomain;
}
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', '');
$denyListDomains = \array_map('trim', explode(',', $denyListDomains));
foreach ($denyListDomains as $denyListDomain) {
if (empty($denyListDomain)) {
continue;
}
$deniedDomains[] = $denyListDomain;
}
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
if (empty($validators)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.');
}
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$owner = '';
if (
($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) ||
($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain))
) {
$owner = 'Appwrite';
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'redirect',
'trigger' => 'manual',
'redirectUrl' => $url,
'redirectStatusCode' => $statusCode,
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $project->getAttribute('region')
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,206 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createSiteRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/site')
->groups(['api', 'proxy'])
->desc('Create site rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
name: 'createSiteRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite Site on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('siteId', '', new UID(), 'ID of site to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$deniedDomains[] = $functionsDomain;
}
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', '');
$denyListDomains = \array_map('trim', explode(',', $denyListDomains));
foreach ($denyListDomains as $denyListDomain) {
if (empty($denyListDomain)) {
continue;
}
$deniedDomains[] = $denyListDomain;
}
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $site->getAttribute('deploymentId', ''));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
if (empty($validators)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.');
}
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$owner = '';
if (
($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) ||
($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain))
) {
$owner = 'Appwrite';
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'deployment',
'trigger' => 'manual',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getInternalId(),
'deploymentVcsProviderBranch' => $branch,
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get(), $branch]),
'owner' => $owner,
'region' => $project->getAttribute('region')
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Proxy\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,23 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Services;
use Appwrite\Platform\Modules\Proxy\Http\Rules\API\Create as CreateAPIRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunctionRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Rules
$this->addAction(CreateAPIRule::getName(), new CreateAPIRule());
$this->addAction(CreateSiteRule::getName(), new CreateSiteRule());
$this->addAction(CreateFunctionRule::getName(), new CreateFunctionRule());
$this->addAction(CreateRedirectRule::getName(), new CreateRedirectRule());
}
}
@@ -0,0 +1,371 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments')
->desc('Create deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
->label('audits.event', 'deployment.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'createDeployment',
description: <<<EOT
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
requestType: ContentType::MULTIPART,
type: MethodType::UPLOAD,
packaging: true,
))
->param('siteId', '', new UID(), 'Site ID.')
->param('installCommand', null, new Text(8192, 0), 'Install Commands.', true)
->param('buildCommand', null, new Text(8192, 0), 'Build Commands.', true)
->param('outputDirectory', null, new Text(8192, 0), 'Output Directory.', true)
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('deviceForSites')
->inject('deviceForLocal')
->inject('queueForBuilds')
->inject('plan')
->callback([$this, 'action']);
}
public function action(
string $siteId,
?string $installCommand,
?string $buildCommand,
?string $outputDirectory,
mixed $code,
mixed $activate,
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Device $deviceForSites,
Device $deviceForLocal,
Build $queueForBuilds,
array $plan
) {
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if ($installCommand === null) {
$installCommand = $site->getAttribute('installCommand', '');
}
if ($buildCommand === null) {
$buildCommand = $site->getAttribute('buildCommand', '');
}
if ($outputDirectory === null) {
$outputDirectory = $site->getAttribute('outputDirectory', '');
}
$file = $request->getFiles('code');
// GraphQL multipart spec adds files with index keys
if (empty($file)) {
$file = $request->getFiles(0);
}
if (empty($file)) {
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
}
$siteSizeLimit = (int) System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000');
if (isset($plan['deploymentSize'])) {
$siteSizeLimit = $plan['deploymentSize'] * 1000 * 1000;
}
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
$fileSizeValidator = new FileSize($siteSizeLimit);
$upload = new Upload();
// Make sure we handle a single file and multiple files the same way
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$contentRange = $request->getHeader('content-range');
$deploymentId = ID::unique();
$chunk = 1;
$chunks = 1;
if (!empty($contentRange)) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
// TODO make `end >= $fileSize` in next breaking version
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
}
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
if ($end === $fileSize - 1 || $end === $fileSize) {
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
$chunks = $chunk = -1;
} else {
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
}
}
if (!$fileSizeValidator->isValid($fileSize) && $siteSizeLimit !== 0) { // Check if file size is exceeding allowed limit
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
}
if (!$upload->isValid($fileTmpName)) {
throw new Exception(Exception::STORAGE_INVALID_FILE);
}
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($chunk === -1) {
$chunk = $chunks;
}
}
$chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
}
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
$commands = [];
if (!empty($installCommand)) {
$commands[] = $installCommand;
}
if (!empty($buildCommand)) {
$commands[] = $buildCommand;
}
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$siteId]),
Query::equal('resourceType', ['sites'])
]);
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
}
}
$fileSize = $deviceForSites->getFileSize($path);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getInternalId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
}
// Start the build
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment);
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
}
}
$metadata = null;
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,138 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
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\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId')
->desc('Delete deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].delete')
->label('audits.event', 'deployment.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'deleteDeployment',
description: <<<EOT
Delete a site deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('deviceForSites')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $deploymentId,
Response $response,
Database $dbForProject,
DeleteEvent $queueForDeletes,
Event $queueForEvents,
Device $deviceForSites
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
}
if (!empty($deployment->getAttribute('sourcePath', ''))) {
if (!($deviceForSites->delete($deployment->getAttribute('sourcePath', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
if ($site->getAttribute('latestDeploymentId') === $deployment->getId()) {
$latestDeployment = $dbForProject->findOne('deployments', [
Query::equal('resourceType', ['sites']),
Query::equal('resourceInternalId', [$site->getInternalId()]),
Query::orderDesc('$createdAt'),
]);
$site = $dbForProject->updateDocument(
'sites',
$site->getId(),
$site
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt())
->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getInternalId())
->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId())
->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''))
);
}
if ($site->getAttribute('deploymentId') === $deployment->getId()) { // Reset site deployment
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
'deploymentId' => '',
'deploymentInternalId' => '',
'deploymentScreenshotDark' => '',
'deploymentScreenshotLight' => '',
'deploymentCreatedAt' => '',
])));
}
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($deployment);
$response->noContent();
}
}
@@ -0,0 +1,151 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Download;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeploymentDownload';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/download')
->desc('Get deployment download')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'getDeploymentDownload',
description: <<<EOT
Get a site deployment content by its unique ID. The endpoint response return with a 'Content-Disposition: attachment' header that tells the browser to start downloading the file to user downloads directory.
EOT,
auth: [AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE
)
],
type: MethodType::LOCATION,
contentType: ContentType::ANY,
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true)
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForSites')
->inject('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $deploymentId,
string $type,
Response $response,
Request $request,
Database $dbForProject,
Device $deviceForSites,
Device $deviceForBuilds
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForSites;
break;
}
if (!$device->exists($path)) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$response
->setContentType('application/gzip')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
$start = $request->getRangeStart();
$end = $request->getRangeEnd();
$unit = $request->getRangeUnit();
if ($end === null) {
$end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1));
}
if ($unit !== 'bytes' || $start >= $end || $end >= $size) {
throw new Exception(Exception::STORAGE_INVALID_RANGE);
}
$response
->addHeader('Accept-Ranges', 'bytes')
->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size)
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($device->read($path, $start, ($end - $start + 1)));
}
if ($size > APP_STORAGE_READ_BUFFER) {
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
$response->chunk(
$device->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
),
(($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size
);
}
} else {
$response->send($device->read($path));
}
}
}
@@ -0,0 +1,182 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
use Utopia\System\System;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDuplicateDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments/duplicate')
->desc('Create duplicate deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('event', 'sites.[siteId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'createDuplicateDeployment',
description: <<<EOT
Create a new build for an existing site deployment. This endpoint allows you to rebuild a deployment with the updated site configuration, including its commands and output directory if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForSites')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $deploymentId,
Request $request,
Response $response,
Document $project,
Database $dbForProject,
Database $dbForPlatform,
Event $queueForEvents,
Build $queueForBuilds,
Device $deviceForSites
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('sourcePath');
if (empty($path) || !$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForSites->transfer($path, $destination, $deviceForSites);
$commands = [];
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'sourcePath' => $destination,
'totalSize' => $deployment->getAttribute('sourceSize', 0),
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'screenshotLight' => '',
'screenshotDark' => '',
'buildStartedAt' => null,
'buildEndedAt' => null,
'buildDuration' => 0,
'buildSize' => 0,
'status' => 'waiting',
'buildPath' => '',
'buildLogs' => '',
'type' => $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual'
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
// Preview deployments for sites
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getInternalId(),
'status' => 'verified',
'certificateId' => '',
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment);
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId')
->desc('Get deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'getDeployment',
description: <<<EOT
Get a site deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,118 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Status;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeploymentStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/status')
->desc('Update deployment status')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'updateDeploymentStatus',
description: <<<EOT
Cancel an ongoing site deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->inject('executor')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $deploymentId,
Response $response,
Database $dbForProject,
Document $project,
Event $queueForEvents,
Executor $executor
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if (\in_array($deployment->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($deployment->getAttribute('buildStartedAt', 'now'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
'buildEndedAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
]));
if ($deployment->getInternalId() === $site->getAttribute('latestDeploymentInternalId', '')) {
$site = $site->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
}
try {
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,213 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Template;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createTemplateDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments/template')
->desc('Create template deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
->label('audits.event', 'deployment.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'createTemplateDeployment',
description: <<<EOT
Create a deployment based on a template.
Use this endpoint with combination of [listTemplates](https://appwrite.io/docs/server/sites#listTemplates) to find the template details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
))
->param('siteId', '', new UID(), 'Site ID.')
->param('repository', '', new Text(128, 0), 'Repository name of the template.')
->param('owner', '', new Text(128, 0), 'The name of the owner of the template.')
->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.')
->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $repository,
string $owner,
string $rootDirectory,
string $version,
bool $activate,
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$template = new Document([
'repositoryName' => $repository,
'ownerName' => $owner,
'rootDirectory' => $rootDirectory,
'version' => $version
]);
if (!empty($site->getAttribute('providerRepositoryId'))) {
$installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId'));
$deployment = $this->redeployVcsSite(
request: $request,
site: $site,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
activate: $activate,
);
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
$commands = [];
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
$deploymentId = ID::unique();
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'type' => 'manual',
'activate' => $activate,
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getInternalId())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getInternalId(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getInternalId(),
'status' => 'verified',
'certificateId' => '',
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment)
->setTemplate($template);
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Vcs;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createVcsDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments/vcs')
->desc('Create VCS deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
->label('audits.event', 'deployment.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'createVcsDeployment',
description: <<<EOT
Create a deployment when a site is connected to VCS.
This endpoint lets you create deployment from a branch, commit, or a tag.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_DEPLOYMENT,
)
],
))
->param('siteId', '', new UID(), 'Site ID.')
// TODO: Support tag in future
->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type of reference passed. Allowed values are: branch, commit')
->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name, commit hash')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $type,
string $reference,
bool $activate,
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId'));
$deployment = $this->redeployVcsSite(
request: $request,
site: $site,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
activate: $activate,
reference: $reference,
referenceType: $type
);
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,124 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listDeployments';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments')
->desc('List deployments')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'deployments',
name: 'listDeployments',
description: <<<EOT
Get a list of all the site's code deployments. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT_LIST,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
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);
}
// Set resource queries
$queries[] = Query::equal('resourceInternalId', [$site->getInternalId()]);
$queries[] = Query::equal('resourceType', ['sites']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$deploymentId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('deployments', $deploymentId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'deployments' => $results,
'total' => $total,
]), Response::MODEL_DEPLOYMENT_LIST);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Frameworks;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listFrameworks';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/frameworks')
->desc('List frameworks')
->groups(['api'])
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'frameworks',
name: 'listFrameworks',
description: <<<EOT
Get a list of all frameworks that are currently available on the server instance.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FRAMEWORK_LIST,
)
]
))
->inject('response')
->callback([$this, 'action']);
}
public function action(Response $response)
{
$frameworks = Config::getParam('frameworks');
foreach ($frameworks as $key => $framework) {
if (!empty($framework['adapters'])) {
$frameworks[$key]['adapters'] = \array_values($framework['adapters']);
}
}
$response->dynamic(new Document([
'total' => count($frameworks),
'frameworks' => \array_values($frameworks)
]), Response::MODEL_FRAMEWORK_LIST);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Logs;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteLog';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId/logs/:logId')
->desc('Delete log')
->groups(['api', 'sites'])
->label('scope', 'log.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].logs.[logId].delete')
->label('audits.event', 'logs.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'logs',
name: 'deleteLog',
description: <<<EOT
Delete a site log by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('logId', '', new UID(), 'Log ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $siteId, string $logId, Response $response, Database $dbForProject, Event $queueForEvents)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$log = $dbForProject->getDocument('executions', $logId);
if ($log->isEmpty()) {
throw new Exception(Exception::LOG_NOT_FOUND);
}
if ($log->getAttribute('resourceType') !== 'sites' && $log->getAttribute('resourceInternalId') !== $site->getInternalId()) {
throw new Exception(Exception::LOG_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('executions', $log->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove log from DB');
}
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('logId', $log->getId())
->setPayload($response->output($log, Response::MODEL_EXECUTION)); // TODO: Update model
$response->noContent();
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Logs;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getLog';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/logs/:logId')
->desc('Get log')
->groups(['api', 'sites'])
->label('scope', 'log.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'logs',
name: 'getLog',
description: <<<EOT
Get a site request log by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('logId', '', new UID(), 'Log ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $logId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty() || !$site->getAttribute('enabled')) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$log = $dbForProject->getDocument('executions', $logId);
if ($log->getAttribute('resourceType') !== 'sites' && $log->getAttribute('resourceInternalId') !== $site->getInternalId()) {
throw new Exception(Exception::LOG_NOT_FOUND);
}
if ($log->isEmpty()) {
throw new Exception(Exception::LOG_NOT_FOUND);
}
$response->dynamic($log, Response::MODEL_EXECUTION); //TODO: Change to model log, but model log already exists - decide what to do
}
}
@@ -0,0 +1,120 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Logs;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Database\Validator\Queries\Logs;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listLogs';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/logs')
->desc('List logs')
->groups(['api', 'sites'])
->label('scope', 'log.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'logs',
name: 'listLogs',
description: <<<EOT
Get a list of all site logs. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION_LIST,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('queries', [], new Logs(), '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(', ', Executions::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, array $queries, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty() || !$site->getAttribute('enabled')) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Set internal queries
$queries[] = Query::equal('resourceInternalId', [$site->getInternalId()]);
$queries[] = Query::equal('resourceType', ['sites']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$logId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('executions', $logId);
if ($cursorDocument->isEmpty() || $cursorDocument->getAttribute('resourceType') !== 'sites') {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Log '{$logId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
]), Response::MODEL_EXECUTION_LIST); // TODO: Update response model
}
}
@@ -0,0 +1,205 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createSite';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites')
->desc('Create site')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].create')
->label('audits.event', 'site.create')
->label('audits.resource', 'site/{response.$id}')
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'create',
description: <<<EOT
Create a new site.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_SITE,
)
],
))
->param('siteId', '', new CustomId(), 'Site ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Site name. Max length: 128 chars.')
->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.')
->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true)
->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true)
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $name,
string $framework,
bool $enabled,
bool $logging,
int $timeout,
string $installCommand,
string $buildCommand,
string $outputDirectory,
string $buildRuntime,
string $adapter,
string $installationId,
string $fallbackFile,
string $providerRepositoryId,
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
Response $response,
Database $dbForProject,
Document $project,
Event $queueForEvents,
Database $dbForPlatform
) {
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
$adapters = \array_keys($configFramework['adapters'] ?? []);
$validator = new WhiteList($adapters, true);
if (!$validator->isValid($adapter)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Adapter not supported for the selected framework.');
}
}
$siteId = ($siteId == 'unique()') ? ID::unique() : $siteId;
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
}
$site = $dbForProject->createDocument('sites', new Document([
'$id' => $siteId,
'enabled' => $enabled,
'live' => true,
'logging' => $logging,
'name' => $name,
'framework' => $framework,
'deploymentInternalId' => '',
'deploymentId' => '',
'timeout' => $timeout,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'search' => implode(' ', [$siteId, $name, $framework]),
'fallbackFile' => $fallbackFile,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => '',
'repositoryInternalId' => '',
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
]));
// Git connect logic
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'site',
'providerPullRequestIds' => []
]));
$site->setAttribute('repositoryId', $repository->getId());
$site->setAttribute('repositoryInternalId', $repository->getInternalId());
}
$site = $dbForProject->updateDocument('sites', $site->getId(), $site);
$queueForEvents->setParam('siteId', $site->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($site, Response::MODEL_SITE);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteSite';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId')
->desc('Delete site')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].delete')
->label('audits.event', 'site.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'delete',
description: <<<EOT
Delete a site by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('siteId', '', new UID(), 'Site ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(
string $siteId,
Response $response,
Database $dbForProject,
DeleteEvent $queueForDeletes,
Event $queueForEvents
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('sites', $site->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove site from DB');
}
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($site);
$queueForEvents->setParam('siteId', $site->getId());
$response->noContent();
}
}
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites\Deployment;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Query;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateSiteDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/sites/:siteId/deployment')
->desc('Update site\'s deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'updateSiteDeployment',
description: <<<EOT
Update the site active deployment. Use this endpoint to switch the code deployment that should be used when visitor opens your site.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SITE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $deploymentId,
Document $project,
Response $response,
Database $dbForProject,
Event $queueForEvents,
Database $dbForPlatform
) {
$site = $dbForProject->getDocument('sites', $siteId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
$oldDeploymentInternalId = $site->getAttribute('deploymentInternalId', '');
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''),
'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''),
'deploymentCreatedAt' => $deployment->getCreatedAt(),
])));
$queries = [
Query::equal('trigger', 'manual'),
Query::equal("type", ["deployment"]),
Query::equal("deploymentResourceType", ["site"]),
Query::equal("deploymentResourceInternalId", [$site->getInternalId()]),
];
if (empty($oldDeploymentInternalId)) {
$queries[] = Query::equal("deploymentInternalId", [""]);
} else {
$queries[] = Query::equal("deploymentInternalId", [$oldDeploymentInternalId]);
}
$this->listRules($project, $queries, $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getInternalId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($site, Response::MODEL_SITE);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getSite';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId')
->desc('Get site')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'get',
description: <<<EOT
Get a site by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SITE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$response->dynamic($site, Response::MODEL_SITE);
}
}
@@ -0,0 +1,282 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateSite';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/sites/:siteId')
->desc('Update site')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].update')
->label('audits.event', 'sites.update')
->label('audits.resource', 'site/{response.$id}')
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'update',
description: <<<EOT
Update site by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SITE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('name', '', new Text(128), 'Site name. Max length: 128 chars.')
->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.')
->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true)
->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true)
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true)
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('executor')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $name,
string $framework,
bool $enabled,
bool $logging,
int $timeout,
string $installCommand,
string $buildCommand,
string $outputDirectory,
string $buildRuntime,
string $adapter,
string $fallbackFile,
string $installationId,
?string $providerRepositoryId,
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
Request $request,
Response $response,
Database $dbForProject,
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
Database $dbForPlatform,
GitHub $github,
Executor $executor
) {
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
$adapters = \array_keys($configFramework['adapters'] ?? []);
$validator = new WhiteList($adapters, true);
if (!$validator->isValid($adapter)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Adapter not supported for the selected framework.');
}
}
// TODO: If only branch changes, re-deploy
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
}
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if (empty($framework)) {
$framework = $site->getAttribute('framework');
}
$enabled ??= $site->getAttribute('enabled', true);
$repositoryId = $site->getAttribute('repositoryId', '');
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
$isConnected = !empty($site->getAttribute('providerRepositoryId', ''));
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$site->getInternalId()]),
Query::equal('resourceType', ['site']),
Query::limit(100),
]);
foreach ($repositories as $repository) {
$dbForPlatform->deleteDocument('repositories', $repository->getId());
}
$providerRepositoryId = '';
$installationId = '';
$providerBranch = '';
$providerRootDirectory = '';
$providerSilentMode = true;
$repositoryId = '';
$repositoryInternalId = '';
}
// Git connect logic
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'site',
'providerPullRequestIds' => []
]));
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getInternalId();
}
$live = true;
if (
$site->getAttribute('name') !== $name ||
$site->getAttribute('buildCommand') !== $buildCommand ||
$site->getAttribute('installCommand') !== $installCommand ||
$site->getAttribute('outputDirectory') !== $outputDirectory ||
$site->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
$site->getAttribute('framework') !== $framework
) {
$live = false;
}
// Enforce Cold Start if spec limits change.
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
'name' => $name,
'framework' => $framework,
'enabled' => $enabled,
'logging' => $logging,
'live' => $live,
'timeout' => $timeout,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $repositoryId,
'repositoryInternalId' => $repositoryInternalId,
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'search' => implode(' ', [$siteId, $name, $framework]),
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
'fallbackFile' => $fallbackFile,
])));
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
$this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
}
$queueForEvents->setParam('siteId', $site->getId());
$response->dynamic($site, Response::MODEL_SITE);
}
}
@@ -0,0 +1,113 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Sites;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listSites';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites')
->desc('List sites')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'sites',
name: 'list',
description: <<<EOT
Get a list of all the project's sites. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SITE_LIST,
)
]
))
->param('queries', [], new Sites(), '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(', ', Sites::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(array $queries, string $search, Response $response, Database $dbForProject)
{
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);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$siteId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('sites', $siteId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Site '{$siteId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$sites = $dbForProject->find('sites', $queries);
$total = $dbForProject->count('sites', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'sites' => $sites,
'total' => $total,
]), Response::MODEL_SITE_LIST);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Specifications;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listSpecifications';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/specifications')
->groups(['api', 'sites'])
->desc('List specifications')
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'frameworks',
name: 'listSpecifications',
description: <<<EOT
List allowed site specifications for this instance.
EOT,
auth: [AuthType::KEY, AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SPECIFICATION_LIST,
)
]
))
->inject('response')
->inject('plan')
->callback([$this, 'action']);
}
public function action(Response $response, array $plan)
{
$allSpecs = Config::getParam('specifications', []);
$specs = [];
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
// Treat 0 as no limit
if ((empty($maxCpus) || $spec['cpus'] <= $maxCpus) && (empty($maxMemory) || $spec['memory'] <= $maxMemory)) {
$specs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $specs,
'total' => count($specs)
]), Response::MODEL_SPECIFICATION_LIST);
}
}
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Templates;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getTemplate';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates/:templateId')
->desc('Get site template')
->groups(['api'])
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'templates',
name: 'getTemplate',
description: <<<EOT
Get a site template using ID. You can use template details in [createSite](/docs/references/cloud/server-nodejs/sites#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_SITE,
)
]
))
->param('templateId', '', new Text(128), 'Template ID.')
->inject('response')
->callback([$this, 'action']);
}
public function action(string $templateId, Response $response)
{
$templates = Config::getParam('templates-site', []);
$allowedTemplates = \array_filter($templates, function ($item) use ($templateId) {
return $item['key'] === $templateId;
});
$template = array_shift($allowedTemplates);
if (empty($template)) {
throw new Exception(Exception::SITE_TEMPLATE_NOT_FOUND);
}
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_SITE);
}
}
@@ -0,0 +1,91 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Templates;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listTemplates';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates')
->desc('List templates')
->groups(['api'])
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'templates',
name: 'listTemplates',
description: <<<EOT
List available site templates. You can use template details in [createSite](/docs/references/cloud/server-nodejs/sites#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_SITE_LIST,
)
]
))
->param('frameworks', [], new ArrayList(new WhiteList(\array_keys(Config::getParam('frameworks')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of frameworks allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' frameworks are allowed.', true)
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools', 'starter', 'databases', 'ai', 'messaging', 'utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true)
->inject('response')
->callback([$this, 'action']);
}
public function action(
array $frameworks,
array $usecases,
int $limit,
int $offset,
Response $response
) {
$templates = Config::getParam('templates-site', []);
if (!empty($frameworks)) {
$templates = \array_filter($templates, function ($template) use ($frameworks) {
return \count(\array_intersect($frameworks, \array_column($template['frameworks'], 'key'))) > 0;
});
}
if (!empty($usecases)) {
$templates = \array_filter($templates, function ($template) use ($usecases) {
return \count(\array_intersect($usecases, $template['useCases'])) > 0;
});
}
\usort($templates, function ($a, $b) {
return $b['score'] <=> $a['score'];
});
$total = \count($templates);
$templates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([
'templates' => $templates,
'total' => $total,
]), Response::MODEL_TEMPLATE_SITE_LIST);
}
}
@@ -0,0 +1,173 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/usage')
->desc('Get site usage')
->groups(['api', 'sites', 'usage'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: null,
name: 'getUsage',
description: <<<EOT
Get usage metrics and statistics for a for a specific site. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_SITE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $range,
Response $response,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_MB_SECONDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_SUCCESS),
str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_SITES, $site->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_FAILED),
str_replace(['{siteInternalId}'], [$site->getInternalId()], METRIC_SITES_ID_REQUESTS),
str_replace(['{siteInternalId}'], [$site->getInternalId()], METRIC_SITES_ID_INBOUND),
str_replace(['{siteInternalId}'], [$site->getInternalId()], METRIC_SITES_ID_OUTBOUND),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$buildsTimeTotal = $usage[$metrics[4]]['total'] ?? 0;
$buildsTotal = $usage[$metrics[2]]['total'] ?? 0;
$response->dynamic(new Document([
'range' => $range,
'deploymentsTotal' => $usage[$metrics[0]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
'buildsTotal' => $buildsTotal,
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
'buildsTimeTotal' => $buildsTimeTotal,
'buildsTimeAverage' => $buildsTotal === 0 ? 0 : (int) ($buildsTimeTotal / $buildsTotal),
'executionsTotal' => $usage[$metrics[5]]['total'],
'executionsTimeTotal' => $usage[$metrics[6]]['total'],
'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'],
'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'buildsSuccessTotal' => $usage[$metrics[9]]['total'],
'buildsFailedTotal' => $usage[$metrics[10]]['total'],
'requestsTotal' => $usage[$metrics[11]]['total'],
'inboundTotal' => $usage[$metrics[12]]['total'],
'outboundTotal' => $usage[$metrics[13]]['total'],
'deployments' => $usage[$metrics[0]]['data'],
'deploymentsStorage' => $usage[$metrics[1]]['data'],
'builds' => $usage[$metrics[2]]['data'],
'buildsStorage' => $usage[$metrics[3]]['data'],
'buildsTime' => $usage[$metrics[4]]['data'],
'executions' => $usage[$metrics[5]]['data'],
'executionsTime' => $usage[$metrics[6]]['data'],
'buildsMbSeconds' => $usage[$metrics[7]]['data'],
'executionsMbSeconds' => $usage[$metrics[8]]['data'],
'buildsSuccess' => $usage[$metrics[9]]['data'],
'buildsFailed' => $usage[$metrics[10]]['data'],
'requests' => $usage[$metrics[11]]['data'],
'inbound' => $usage[$metrics[12]]['data'],
'outbound' => $usage[$metrics[13]]['data'],
]), Response::MODEL_USAGE_SITE);
}
}
@@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Usage;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'getSitesUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/usage')
->desc('Get sites usage')
->groups(['api', 'sites', 'usage'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: null,
name: 'listUsage',
description: <<<EOT
Get usage metrics and statistics for all sites in the project. View statistics including total deployments, builds, logs, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_SITES,
)
]
))
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $range, Response $response, Database $dbForProject)
{
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
METRIC_SITES,
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_DEPLOYMENTS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS_STORAGE),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS_COMPUTE),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_EXECUTIONS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS_MB_SECONDS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS_SUCCESS),
str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS_FAILED),
METRIC_SITES_REQUESTS,
METRIC_SITES_INBOUND,
METRIC_SITES_OUTBOUND,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'sitesTotal' => $usage[$metrics[0]]['total'],
'deploymentsTotal' => $usage[$metrics[1]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
'buildsTotal' => $usage[$metrics[3]]['total'],
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
'executionsTotal' => $usage[$metrics[6]]['total'],
'executionsTimeTotal' => $usage[$metrics[7]]['total'],
'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'],
'buildsSuccessTotal' => $usage[$metrics[10]]['total'],
'buildsFailedTotal' => $usage[$metrics[11]]['total'],
'requestsTotal' => $usage[$metrics[12]]['total'],
'inboundTotal' => $usage[$metrics[13]]['total'],
'outboundTotal' => $usage[$metrics[14]]['total'],
'sites' => $usage[$metrics[0]]['data'],
'deployments' => $usage[$metrics[1]]['data'],
'deploymentsStorage' => $usage[$metrics[2]]['data'],
'builds' => $usage[$metrics[3]]['data'],
'buildsStorage' => $usage[$metrics[4]]['data'],
'buildsTime' => $usage[$metrics[5]]['data'],
'executions' => $usage[$metrics[6]]['data'],
'executionsTime' => $usage[$metrics[7]]['data'],
'buildsMbSeconds' => $usage[$metrics[8]]['data'],
'executionsMbSeconds' => $usage[$metrics[9]]['data'],
'buildsSuccess' => $usage[$metrics[10]]['data'],
'buildsFailed' => $usage[$metrics[11]]['data'],
'requests' => $usage[$metrics[12]]['data'],
'inbound' => $usage[$metrics[13]]['data'],
'outbound' => $usage[$metrics[14]]['data'],
]), Response::MODEL_USAGE_SITES);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/variables')
->desc('Create variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('audits.event', 'variable.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'variables',
name: 'createVariable',
description: <<<EOT
Create a new site variable. These variables can be accessed during build and runtime (server-side rendering) as environment variables.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
]
))
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
->callback([$this, 'action']);
}
public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variableId = ID::unique();
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'site',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $site->getId(), $key, 'site']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Delete variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('audits.event', 'variable.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
group: 'variables',
name: 'deleteVariable',
description: <<<EOT
Delete a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response->noContent();
}
}
@@ -0,0 +1,83 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Get variable')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label(
'sdk',
new Method(
namespace: 'sites',
group: 'variables',
name: 'getVariable',
description: <<<EOT
Get a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
],
)
)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if (
$variable === false ||
$variable->isEmpty() ||
$variable->getAttribute('resourceInternalId') !== $site->getInternalId() ||
$variable->getAttribute('resourceType') !== 'site'
) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,104 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Update variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('audits.event', 'variable.update')
->label('audits.resource', 'site/{request.siteId}')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
group: 'variables',
name: 'updateVariable',
description: <<<EOT
Update variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $siteId,
string $variableId,
string $key,
?string $value,
?bool $secret,
Response $response,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable->getAttribute('secret') === true && $secret === false) {
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/variables')
->desc('List variables')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label(
'sdk',
new Method(
namespace: 'sites',
group: 'variables',
name: 'listVariables',
description: <<<EOT
Get a list of all variables of a specific site.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
],
)
)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(
string $siteId,
Response $response,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$response->dynamic(new Document([
'variables' => $site->getAttribute('vars', []),
'total' => \count($site->getAttribute('vars', [])),
]), Response::MODEL_VARIABLE_LIST);
}
}

Some files were not shown because too many files have changed in this diff Show More