Merge branch 'refs/heads/1.6.x' into feat-extract-team-deletion

This commit is contained in:
Binyamin Yawitz
2024-08-08 09:58:30 -04:00
4991 changed files with 81113 additions and 669 deletions
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
use Utopia\Validator\Text;
/**
* MockNumber.
*
* Validates if a given object represents a valid phone and OTP pair
*/
class MockNumber extends Validator
{
private $message = '';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_array($value) || !isset($value['phone']) || !isset($value['otp'])) {
$this->message = 'Invalid payload structure. Please check the "phone" and "otp" fields';
return false;
}
$phone = new Phone();
if (!$phone->isValid($value['phone'])) {
$this->message = $phone->getDescription();
return false;
}
$otp = new Text(6, 6, Text::NUMBERS);
if (!$otp->isValid($value['otp'])) {
$this->message = 'Invalid OTP. Please make sure the OTP is a 6 digit number';
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}
+24
View File
@@ -14,6 +14,7 @@ class Func extends Event
protected string $path = '';
protected string $method = '';
protected array $headers = [];
protected ?string $functionId = null;
protected ?Document $function = null;
protected ?Document $execution = null;
@@ -49,6 +50,28 @@ class Func extends Event
return $this->function;
}
/**
* Sets function id for the function event.
*
* @param string $functionId
*/
public function setFunctionId(string $functionId): self
{
$this->functionId = $functionId;
return $this;
}
/**
* Returns set function id for the function event.
*
* @return string|null
*/
public function getFunctionId(): ?string
{
return $this->functionId;
}
/**
* Sets execution for the function event.
*
@@ -200,6 +223,7 @@ class Func extends Event
'project' => $this->project,
'user' => $this->user,
'function' => $this->function,
'functionId' => $this->functionId,
'execution' => $this->execution,
'type' => $this->type,
'jwt' => $this->jwt,
+2
View File
@@ -156,6 +156,7 @@ class Exception extends \Exception
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';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
@@ -168,6 +169,7 @@ class Exception extends \Exception
/** Execution */
public const EXECUTION_NOT_FOUND = 'execution_not_found';
public const EXECUTION_IN_PROGRESS = 'execution_in_progress';
/** Databases */
public const DATABASE_NOT_FOUND = 'database_not_found';
+3 -1
View File
@@ -86,6 +86,7 @@ abstract class Migration
'1.5.5' => 'V20',
'1.5.6' => 'V20',
'1.5.7' => 'V20',
'1.6.0' => 'V21'
];
/**
@@ -239,7 +240,7 @@ abstract class Migration
}
/**
* Creates colletion from the config collection.
* Creates collection from the config collection.
*
* @param string $id
* @param string|null $name
@@ -266,6 +267,7 @@ abstract class Migration
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'default' => $attribute['default'] ?? null,
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
+26
View File
@@ -336,6 +336,32 @@ class V20 extends Migration
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
break;
case 'topics':
try {
$this->projectDB->updateAttributeDefault($id, 'emailTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'pushTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'smsTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
break;
}
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
class V21 extends Migration
{
/**
* @throws Throwable
*/
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
);
}
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
}
/**
* Migrate Collections.
*
* @return void
* @throws Exception|Throwable
*/
private function migrateCollections(): void
{
$internalProjectId = $this->project->getInternalId();
$collectionType = match ($internalProjectId) {
'console' => 'console',
default => 'projects',
};
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
switch ($id) {
case 'projects':
// Create accessedAt attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
} catch (Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
break;
case 'schedules':
// Create data attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'data');
} catch (Throwable $th) {
Console::warning("'data' from {$id}: {$th->getMessage()}");
}
break;
case 'functions':
// Create scopes attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'scopes');
} catch (Throwable $th) {
Console::warning("'scopes' from {$id}: {$th->getMessage()}");
}
// Create size attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'size');
} catch (Throwable $th) {
Console::warning("'size' from {$id}: {$th->getMessage()}");
}
break;
case 'executions':
// Create requestMethod index
try {
$this->createIndexFromCollection($this->projectDB, $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');
} catch (\Throwable $th) {
Console::warning("'_key_requestPath' from {$id}: {$th->getMessage()}");
}
// Create deployment index
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_deployment');
} catch (\Throwable $th) {
Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'scheduledAt' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduledAt');
} catch (\Throwable $th) {
Console::warning("'scheduledAt' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'scheduleInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId');
} catch (\Throwable $th) {
Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'scheduleId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleId');
} catch (\Throwable $th) {
Console::warning("'scheduleId' from {$id}: {$th->getMessage()}");
}
}
usleep(50000);
}
}
/**
* Fix run on each document
*
* @param Document $document
* @return Document
*/
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'projects':
/**
* Bump version number.
*/
$document->setAttribute('version', '1.6.0');
// Add accessedAt attribute
$document->setAttribute('accessedAt', DateTime::now());
break;
case 'functions':
// Add scopes attribute
$document->setAttribute('scopes', []);
// Add size attribute
$document->setAttribute('size', 's-1vcpu-512m');
}
return $document;
}
}
+2
View File
@@ -8,6 +8,7 @@ use Appwrite\Platform\Tasks\Maintenance;
use Appwrite\Platform\Tasks\Migrate;
use Appwrite\Platform\Tasks\QueueCount;
use Appwrite\Platform\Tasks\QueueRetry;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\Platform\Tasks\ScheduleFunctions;
use Appwrite\Platform\Tasks\ScheduleMessages;
use Appwrite\Platform\Tasks\SDKs;
@@ -33,6 +34,7 @@ class Tasks extends Service
->addAction(SDKs::getName(), new SDKs())
->addAction(SSL::getName(), new SSL())
->addAction(ScheduleFunctions::getName(), new ScheduleFunctions())
->addAction(ScheduleExecutions::getName(), new ScheduleExecutions())
->addAction(ScheduleMessages::getName(), new ScheduleMessages())
->addAction(Specs::getName(), new Specs())
->addAction(Upgrade::getName(), new Upgrade())
+1
View File
@@ -7,6 +7,7 @@ use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
+5 -2
View File
@@ -50,7 +50,7 @@ class SDKs extends Action
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
$message = ($git) ? Console::confirm('Please enter your commit message:') : '';
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', 'latest'])) {
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', '1.6.x', 'latest'])) {
throw new \Exception('Unknown version given');
}
@@ -235,7 +235,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setTwitter(APP_SOCIAL_TWITTER_HANDLE)
->setDiscord(APP_SOCIAL_DISCORD_CHANNEL, APP_SOCIAL_DISCORD)
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '1.5.0',
'X-Appwrite-Response-Format' => '1.6.0',
]);
// Make sure we have a clean slate.
@@ -252,6 +252,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$gitUrl = $language['gitUrl'];
$gitBranch = $language['gitBranch'];
// TEMPORARY
$gitBranch = '1.6.x';
if (!$production) {
$gitUrl = 'git@github.com:aw-tests/' . $language['gitRepoName'] . '.git';
}
+4 -2
View File
@@ -64,7 +64,8 @@ abstract class ScheduleBase extends Action
$collectionId = match ($schedule->getAttribute('resourceType')) {
'function' => 'functions',
'message' => 'messages'
'message' => 'messages',
'execution' => 'executions'
};
$resource = $getProjectDB($project)->getDocument(
@@ -113,7 +114,8 @@ abstract class ScheduleBase extends Action
} catch (\Throwable $th) {
$collectionId = match ($document->getAttribute('resourceType')) {
'function' => 'functions',
'message' => 'messages'
'message' => 'messages',
'execution' => 'executions'
};
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Func;
use Utopia\Database\Database;
use Utopia\Pools\Group;
class ScheduleExecutions extends ScheduleBase
{
public const UPDATE_TIMER = 3; // seconds
public const ENQUEUE_TIMER = 4; // seconds
public static function getName(): string
{
return 'schedule-executions';
}
public static function getSupportedResource(): string
{
return 'execution';
}
protected function enqueueResources(Group $pools, Database $dbForConsole): void
{
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForFunctions = new Func($connection);
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
$dbForConsole->deleteDocument(
'schedules',
$schedule['$id'],
);
unset($this->schedules[$schedule['resourceId']]);
continue;
}
$now = new \DateTime();
$scheduledAt = new \DateTime($schedule['schedule']);
if ($scheduledAt > $now) {
continue;
}
$queueForFunctions
->setType('schedule')
// Set functionId instead of function as we don't have $dbForProject
// TODO: Refactor to use function instead of functionId
->setFunctionId($schedule['resource']['functionId'])
->setExecution($schedule['resource'])
->setMethod($schedule['data']['method'] ?? 'POST')
->setPath($schedule['data']['path'] ?? '/')
->setHeaders($schedule['data']['headers'] ?? [])
->setBody($schedule['data']['body'] ?? '')
->setProject($schedule['project'])
->trigger();
$dbForConsole->deleteDocument(
'schedules',
$schedule['$id'],
);
unset($this->schedules[$schedule['resourceId']]);
}
$queue->reclaim();
}
}
@@ -35,7 +35,7 @@ class ScheduleMessages extends ScheduleBase
continue;
}
\go(function () use ($now, $schedule, $pools, $dbForConsole) {
\go(function () use ($schedule, $pools, $dbForConsole) {
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForMessaging = new Messaging($connection);
+8
View File
@@ -333,6 +333,7 @@ class Builds extends Action
$source = $path;
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source));
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
}
@@ -400,6 +401,8 @@ class Builds extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
$command = $deployment->getAttribute('commands', '');
@@ -490,6 +493,11 @@ class Builds extends Action
$endTime = DateTime::now();
$durationEnd = \microtime(true);
$buildSizeLimit = (int) System::getEnv('_APP_FUNCTIONS_BUILD_SIZE_LIMIT', '2000000000');
if ($response['size'] > $buildSizeLimit) {
throw new \Exception('Build size should be less than ' . number_format($buildSizeLimit / 1048576, 2) . ' MBs.');
}
/** Update the build document */
$build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime']))));
$build->setAttribute('endTime', $endTime);
+5 -21
View File
@@ -439,40 +439,24 @@ class Certificates extends Action
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
// Send mail to administratore mail
// Send mail to administrator mail
$template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-certificate-failed.tpl');
$template->setParam('{{domain}}', $domain);
$template->setParam('{{error}}', \nl2br($errorMessage));
$template->setParam('{{attempts}}', $attempt);
// TODO: Use setbodyTemplate once #7307 is merged
$subject = 'Certificate failed to generate';
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
$subject = \sprintf($locale->getText("emails.certificate.subject"), $domain);
$message = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-inner-base.tpl');
$message
->setParam('{{body}}', $locale->getText("emails.certificate.body"), escapeHtml: false)
->setParam('{{hello}}', $locale->getText("emails.certificate.hello"))
->setParam('{{footer}}', $locale->getText("emails.certificate.footer"))
->setParam('{{thanks}}', $locale->getText("emails.certificate.thanks"))
->setParam('{{signature}}', $locale->getText("emails.certificate.signature"));
$body = $message->render();
$body = $template->render();
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
'domain' => $domain,
'error' => '<br><pre>' . $errorMessage . '</pre>',
'attempt' => $attempt,
'project' => 'Console',
'redirect' => 'https://' . $domain,
];
$subject = \sprintf($locale->getText("emails.certificate.subject"), $domain);
$queueForMails
->setSubject($subject)
->setBody($body)
->setName('Appwrite Administrator')
->setbodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
->setVariables($emailVariables)
->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')))
->trigger();
+19 -11
View File
@@ -83,6 +83,7 @@ class Functions extends Action
$eventData = $payload['payload'] ?? '';
$project = new Document($payload['project'] ?? []);
$function = new Document($payload['function'] ?? []);
$functionId = $payload['functionId'] ?? '';
$user = new Document($payload['user'] ?? []);
$method = $payload['method'] ?? 'POST';
$headers = $payload['headers'] ?? [];
@@ -92,6 +93,10 @@ class Functions extends Action
return;
}
if ($function->isEmpty() && !empty($functionId)) {
$function = $dbForProject->getDocument('functions', $functionId);
}
$log->addTag('functionId', $function->getId());
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
@@ -176,6 +181,7 @@ class Functions extends Action
);
break;
case 'schedule':
$execution = new Document($payload['execution'] ?? []);
$this->execute(
log: $log,
dbForProject: $dbForProject,
@@ -193,7 +199,7 @@ class Functions extends Action
jwt: null,
event: null,
eventData: null,
executionId: null,
executionId: $execution->getId() ?? null
);
break;
}
@@ -367,6 +373,9 @@ class Functions extends Action
$headers['x-appwrite-event'] = $event ?? '';
$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';
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
@@ -399,9 +408,7 @@ class Functions extends Action
'search' => implode(' ', [$functionId, $executionId]),
]);
if ($function->getAttribute('logging')) {
$execution = $dbForProject->createDocument('executions', $execution);
}
$execution = $dbForProject->createDocument('executions', $execution);
// TODO: @Meldiron Trigger executions.create event here
@@ -413,9 +420,7 @@ class Functions extends Action
if ($execution->getAttribute('status') !== 'processing') {
$execution->setAttribute('status', 'processing');
if ($function->getAttribute('logging')) {
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
}
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
}
$durationStart = \microtime(true);
@@ -462,6 +467,8 @@ class Functions extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
]);
/** Execute function */
@@ -483,7 +490,8 @@ class Functions extends Action
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command
runtimeEntrypoint: $command,
logging: $function->getAttribute('logging', true),
);
$status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed';
@@ -525,9 +533,9 @@ class Functions extends Action
;
}
if ($function->getAttribute('logging')) {
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
}
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
/** Trigger Webhook */
$executionModel = new Execution();
$queueForEvents
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace Appwrite\Utopia\Fetch;
class BodyMultipart
{
/**
* @var array<string, mixed> $parts
*/
private array $parts = [];
private string $boundary = "";
public function __construct(string $boundary = null)
{
if (is_null($boundary)) {
$this->boundary = self::generateBoundary();
} else {
$this->boundary = $boundary;
}
}
public static function generateBoundary(): string
{
return '-----------------------------' . \uniqid();
}
public function load(string $body): self
{
$eol = "\r\n";
$sections = \explode('--' . $this->boundary, $body);
foreach ($sections as $section) {
if (empty($section)) {
continue;
}
if (strpos($section, $eol) === 0) {
$section = substr($section, \strlen($eol));
}
if (substr($section, -2) === $eol) {
$section = substr($section, 0, -1 * \strlen($eol));
}
if ($section == '--') {
continue;
}
$partChunks = \explode($eol . $eol, $section, 2);
if (\count($partChunks) < 2) {
continue; // Broken part
}
[ $partHeaders, $partBody ] = $partChunks;
$partHeaders = \explode($eol, $partHeaders);
$partName = "";
foreach ($partHeaders as $partHeader) {
if (!empty($partName)) {
break;
}
$partHeaderArray = \explode(':', $partHeader, 2);
$partHeaderName = \strtolower($partHeaderArray[0] ?? '');
$partHeaderValue = $partHeaderArray[1] ?? '';
if ($partHeaderName == "content-disposition") {
$dispositionChunks = \explode("; ", $partHeaderValue);
foreach ($dispositionChunks as $dispositionChunk) {
$dispositionChunkValues = \explode("=", $dispositionChunk, 2);
if (\count($dispositionChunkValues) >= 2) {
if ($dispositionChunkValues[0] === "name") {
$partName = \trim($dispositionChunkValues[1], "\"");
break;
}
}
}
}
}
if (!empty($partName)) {
$this->parts[$partName] = $partBody;
}
}
return $this;
}
/**
* @return array<string, mixed>
*/
public function getParts(): array
{
return $this->parts ?? [];
}
public function getPart(string $key, mixed $default = ''): mixed
{
return $this->parts[$key] ?? $default;
}
public function setPart(string $key, mixed $value): self
{
$this->parts[$key] = $value;
return $this;
}
public function getBoundary(): string
{
return $this->boundary;
}
public function setBoundary(string $boundary): self
{
$this->boundary = $boundary;
return $this;
}
public function exportHeader(): string
{
return 'multipart/form-data; boundary=' . $this->boundary;
}
public function exportBody(): string
{
$eol = "\r\n";
$query = '--' . $this->boundary;
foreach ($this->parts as $key => $value) {
$query .= $eol . 'Content-Disposition: form-data; name="' . $key . '"';
if (\is_array($value)) {
$query .= $eol . 'Content-Type: application/json';
$value = \json_encode($value);
}
$query .= $eol . $eol;
if ($value === false) {
$query .= 0 . $eol;
} else {
$query .= $value . $eol;
}
$query .= '--' . $this->boundary;
}
$query .= "--" . $eol;
return $query;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
class V18 extends Filter
{
// Convert 1.5 params to 1.6
public function parse(array $content, string $model): array
{
switch ($model) {
case 'account.deleteMfaAuthenticator':
unset($content['otp']);
break;
}
return $content;
}
}
+14
View File
@@ -72,6 +72,7 @@ use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
@@ -86,7 +87,10 @@ use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\UsageBuckets;
@@ -250,6 +254,10 @@ class Response extends SwooleResponse
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
public const MODEL_HEADERS = 'headers';
public const MODEL_TEMPLATE_FUNCTION = 'templateFunction';
public const MODEL_TEMPLATE_FUNCTION_LIST = 'templateFunctionList';
public const MODEL_TEMPLATE_RUNTIME = 'templateRuntime';
public const MODEL_TEMPLATE_VARIABLE = 'templateVariable';
// Proxy
public const MODEL_PROXY_RULE = 'proxyRule';
@@ -269,6 +277,7 @@ class Response extends SwooleResponse
public const MODEL_WEBHOOK_LIST = 'webhookList';
public const MODEL_KEY = 'key';
public const MODEL_KEY_LIST = 'keyList';
public const MODEL_MOCK_NUMBER = 'mockNumber';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM = 'platform';
@@ -338,6 +347,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION))
->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION))
->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY))
->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH))
@@ -407,6 +417,9 @@ class Response extends SwooleResponse
->setModel(new Team())
->setModel(new Membership())
->setModel(new Func())
->setModel(new TemplateFunction())
->setModel(new TemplateRuntime())
->setModel(new TemplateVariable())
->setModel(new Installation())
->setModel(new ProviderRepository())
->setModel(new Detection())
@@ -419,6 +432,7 @@ class Response extends SwooleResponse
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
->setModel(new MockNumber())
->setModel(new AuthProvider())
->setModel(new Platform())
->setModel(new Variable())
@@ -0,0 +1,43 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
class V18 extends Filter
{
// Convert 1.6 Data format to 1.5 format
public function parse(array $content, string $model): array
{
$parsedResponse = $content;
$parsedResponse = match($model) {
Response::MODEL_FUNCTION => $this->parseFunction($content),
Response::MODEL_EXECUTION => $this->parseExecution($content),
Response::MODEL_PROJECT => $this->parseProject($content),
default => $parsedResponse,
};
return $parsedResponse;
}
protected function parseExecution(array $content)
{
unset($content['scheduledAt']);
return $content;
}
protected function parseFunction(array $content)
{
unset($content['scopes']);
return $content;
}
protected function parseProject(array $content)
{
unset($content['authMockNumbers']);
unset($content['authSessionAlerts']);
return $content;
}
}
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\Role;
class Execution extends Model
@@ -110,6 +111,13 @@ class Execution extends Model
'default' => 0,
'example' => 0.400,
])
->addRule('scheduledAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The scheduled time for execution. If left empty, execution will be queued immediately.',
'required' => false,
'default' => DateTime::now(),
'example' => self::TYPE_DATETIME_EXAMPLE,
])
;
}
+1 -1
View File
@@ -119,7 +119,7 @@ class Func extends Model
->addRule('version', [
'type' => self::TYPE_STRING,
'description' => 'Version of Open Runtimes used for the function.',
'default' => 'v3',
'default' => 'v4',
'example' => 'v2',
])
->addRule('installationId', [
+1 -1
View File
@@ -60,7 +60,7 @@ class Key extends Model
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_KEY_ACCCESS / 60 / 60 . ' hours.',
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_KEY_ACCESS / 60 / 60 . ' hours.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE
])
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class MockNumber extends Model
{
public function __construct()
{
$this
->addRule('phone', [
'type' => self::TYPE_STRING,
'description' => 'Mock phone number for testing phone authentication. Useful for testing phone authentication without sending an SMS.',
'default' => '',
'example' => '+1612842323',
])
->addRule('otp', [
'type' => self::TYPE_STRING,
'description' => 'Mock OTP for the number. ',
'default' => '',
'example' => '123456',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Mock Number';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MOCK_NUMBER;
}
}
@@ -138,6 +138,19 @@ class Project extends Model
'default' => false,
'example' => true,
])
->addRule('authMockNumbers', [
'type' => Response::MODEL_MOCK_NUMBER,
'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).',
'default' => [],
'array' => true,
'example' => [new \stdClass()],
])
->addRule('authSessionAlerts', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to send session alert emails to users.',
'default' => false,
'example' => true,
])
->addRule('oAuthProviders', [
'type' => Response::MODEL_AUTH_PROVIDER,
'description' => 'List of Auth Providers.',
@@ -321,6 +334,8 @@ class Project extends Model
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
$document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []);
$document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false);
foreach ($auth as $index => $method) {
$key = $method['key'];
@@ -0,0 +1,135 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateFunction extends Model
{
public function __construct()
{
$this
->addRule('icon', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Icon.',
'default' => '',
'example' => 'icon-lightning-bolt',
])
->addRule('id', [
'type' => self::TYPE_STRING,
'description' => 'Function Template ID.',
'default' => '',
'example' => 'starter',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Name.',
'default' => '',
'example' => 'Starter function',
])
->addRule('tagline', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Tagline.',
'default' => '',
'example' => 'A simple function to get started.',
])
->addRule('permissions', [
'type' => self::TYPE_STRING,
'description' => 'Execution permissions.',
'default' => [],
'example' => 'any',
'array' => true,
])
->addRule('events', [
'type' => self::TYPE_STRING,
'description' => 'Function trigger events.',
'default' => [],
'example' => 'account.create',
'array' => true,
])
->addRule('cron', [
'type' => self::TYPE_STRING,
'description' => 'Function execution schedult in CRON format.',
'default' => '',
'example' => '0 0 * * *',
])
->addRule('timeout', [
'type' => self::TYPE_INTEGER,
'description' => 'Function execution timeout in seconds.',
'default' => 15,
'example' => 300,
])
->addRule('useCases', [
'type' => self::TYPE_STRING,
'description' => 'Function use cases.',
'default' => [],
'example' => 'Starter',
'array' => true,
])
->addRule('runtimes', [
'type' => Response::MODEL_TEMPLATE_RUNTIME,
'description' => 'List of runtimes that can be used with this template.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('instructions', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Instructions.',
'default' => '',
'example' => 'For documentation and instructions check out <link>.',
])
->addRule('vcsProvider', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Provider.',
'default' => '',
'example' => 'github',
])
->addRule('providerRepositoryId', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Repository ID',
'default' => '',
'example' => 'templates',
])
->addRule('providerOwner', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Owner.',
'default' => '',
'example' => 'appwrite',
])
->addRule('providerBranch', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) branch name',
'default' => '',
'example' => 'main',
])
->addRule('variables', [
'type' => Response::MODEL_TEMPLATE_VARIABLE,
'description' => 'Function variables.',
'default' => [],
'example' => [],
'array' => true
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Function';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_FUNCTION;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateRuntime extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Runtime Name.',
'default' => '',
'example' => 'node-19.0',
])
->addRule('commands', [
'type' => self::TYPE_STRING,
'description' => 'The build command used to build the deployment.',
'default' => '',
'example' => 'npm install',
])
->addRule('entrypoint', [
'type' => self::TYPE_STRING,
'description' => 'The entrypoint file used to execute the deployment.',
'default' => '',
'example' => 'index.js',
])
->addRule('providerRootDirectory', [
'type' => self::TYPE_STRING,
'description' => 'Path to function in VCS (Version Control System) repository',
'default' => '',
'example' => 'node/starter',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Runtime';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_RUNTIME;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateVariable extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Variable Name.',
'default' => '',
'example' => 'APPWRITE_DATABASE_ID',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Variable Description.',
'default' => '',
'example' => 'The ID of the Appwrite database that contains the collection to sync.',
])
->addRule('value', [
'type' => self::TYPE_STRING,
'description' => 'Variable Value.',
'default' => '',
'example' => '512',
])
->addRule('placeholder', [
'type' => self::TYPE_STRING,
'description' => 'Variable Placeholder.',
'default' => '',
'example' => '64a55...7b912',
])
->addRule('required', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is the variable required?',
'default' => false,
'example' => false,
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Variable Type.',
'default' => '',
'example' => 'password',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Variable';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_VARIABLE;
}
}
+1 -1
View File
@@ -135,7 +135,7 @@ class User extends Model
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCCESS / 60 / 60 . ' hours.',
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCESS / 60 / 60 . ' hours.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
+45 -5
View File
@@ -3,6 +3,7 @@
namespace Executor;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Fetch\BodyMultipart;
use Exception;
use Utopia\System\System;
@@ -74,6 +75,12 @@ class Executor
$runtimeId = "$projectId-$deploymentId-build";
$route = "/runtimes";
$timeout = (int) System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
// Remove after migration
if ($version == 'v3') {
$version = 'v4';
}
$params = [
'runtimeId' => $runtimeId,
'source' => $source,
@@ -161,6 +168,7 @@ class Executor
* @param string $source
* @param string $entrypoint
* @param string $runtimeEntrypoint
* @param bool $logging
*
* @return array
*/
@@ -178,6 +186,7 @@ class Executor
string $method,
array $headers,
string $runtimeEntrypoint = null,
bool $logging,
int $requestTimeout = null
) {
if (empty($headers['host'])) {
@@ -185,11 +194,16 @@ class Executor
}
$runtimeId = "$projectId-$deploymentId";
$route = '/runtimes/' . $runtimeId . '/execution';
$route = '/runtimes/' . $runtimeId . '/executions';
// Remove after migration
if ($version == 'v3') {
$version = 'v4';
}
$params = [
'runtimeId' => $runtimeId,
'variables' => $variables,
'body' => $body,
'timeout' => $timeout,
'path' => $path,
'method' => $method,
@@ -201,15 +215,21 @@ class Executor
'memory' => $this->memory,
'version' => $version,
'runtimeEntrypoint' => $runtimeEntrypoint,
'logging' => $logging,
'restartPolicy' => 'always' // Once utopia/orchestration has it, use DockerAPI::ALWAYS (0.13+)
];
if(!empty($body)) {
$params['body'] = $body;
}
// Safety timeout. Executor has timeout, and open runtime has soft timeout.
// This one shouldn't really happen, but prevents from unexpected networking behaviours.
if ($requestTimeout == null) {
$requestTimeout = $timeout + 15;
}
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $requestTimeout);
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@@ -217,6 +237,11 @@ class Executor
throw new \Exception($message, $status);
}
$response['body']['headers'] = \json_decode($response['body']['headers'] ?? '{}', true);
$response['body']['statusCode'] = \intval($response['body']['statusCode'] ?? 500);
$response['body']['duration'] = \floatval($response['body']['duration'] ?? 0);
$response['body']['startTime'] = \floatval($response['body']['startTime'] ?? \microtime(true));
return $response['body'];
}
@@ -248,7 +273,13 @@ class Executor
break;
case 'multipart/form-data':
$query = $this->flatten($params);
$multipart = new BodyMultipart();
foreach ($params as $key => $value) {
$multipart->setPart($key, $value);
}
$headers['content-type'] = $multipart->exportHeader();
$query = $multipart->exportBody();
break;
default:
@@ -315,7 +346,16 @@ class Executor
$curlErrorMessage = curl_error($ch);
if ($decode) {
switch (substr($responseType, 0, strpos($responseType, ';'))) {
$strpos = strpos($responseType, ';');
$strpos = \is_bool($strpos) ? \strlen($responseType) : $strpos;
switch (substr($responseType, 0, $strpos)) {
case 'multipart/form-data':
$boundary = \explode('boundary=', $responseHeaders['content-type'] ?? '')[1] ?? '';
$multipartResponse = new BodyMultipart($boundary);
$multipartResponse->load(\is_bool($responseBody) ? '' : $responseBody);
$responseBody = $multipartResponse->getParts();
break;
case 'application/json':
$json = json_decode($responseBody, true);