Merge branch 'master' of github.com:appwrite/appwrite into refactor-cache-poc

 Conflicts:
	app/config/collections.php
	app/controllers/api/storage.php
	app/controllers/shared/api.php
	app/init.php
	app/workers/deletes.php
This commit is contained in:
shimon
2023-09-05 19:19:59 +03:00
3497 changed files with 72971 additions and 12165 deletions
+6 -4
View File
@@ -9,7 +9,6 @@ use Appwrite\Auth\Hash\Phpass;
use Appwrite\Auth\Hash\Scrypt;
use Appwrite\Auth\Hash\Scryptmodified;
use Appwrite\Auth\Hash\Sha;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\Role;
@@ -456,13 +455,16 @@ class Auth
}
}
foreach ($user->getAttribute('labels', []) as $label) {
$roles[] = 'label:' . $label;
}
return $roles;
}
public static function isAnonymousUser(Document $user): bool
{
return (is_null($user->getAttribute('email'))
|| is_null($user->getAttribute('phone'))
) && is_null($user->getAttribute('password'));
return is_null($user->getAttribute('email'))
&& is_null($user->getAttribute('phone'));
}
}
+17 -2
View File
@@ -2,6 +2,8 @@
namespace Appwrite\Auth;
use Appwrite\Auth\OAuth2\Exception;
abstract class OAuth2
{
/**
@@ -73,6 +75,13 @@ abstract class OAuth2
*/
abstract public function refreshTokens(string $refreshToken): array;
/**
* @param string $accessToken
*
* @return string
*/
abstract public function getUserID(string $accessToken): string;
/**
* @param string $accessToken
*
@@ -148,11 +157,11 @@ abstract class OAuth2
*
* @return string
*/
public function getAccessTokenExpiry(string $code): string
public function getAccessTokenExpiry(string $code): int
{
$tokens = $this->getTokens($code);
return $tokens['expires_in'] ?? '';
return $tokens['expires_in'] ?? 0;
}
// The parseState function was designed specifically for Amazon OAuth2 Adapter to override.
@@ -195,8 +204,14 @@ abstract class OAuth2
// Send the request & save response to $response
$response = \curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
\curl_close($ch);
if ($code >= 400) {
throw new Exception($response, $code);
}
return (string)$response;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Extend\Exception as AppwriteException;
class Exception extends AppwriteException
{
protected string $response = '';
protected string $error = '';
protected string $errorDescription = '';
public function __construct(string $response = '', int $code = 0, \Throwable $previous = null)
{
$this->response = $response;
$this->message = $response;
$decoded = json_decode($response, true);
if (\is_array($decoded)) {
if (\is_array($decoded['error'] ?? '')) {
$this->error = $decoded['error']['status'];
$this->errorDescription = $decoded['error']['message'];
$this->message = $this->error . ': ' . $this->errorDescription;
} else {
$this->error = $decoded['error'] ?? $decoded['message'] ?? 'Unknown error';
$this->errorDescription = $decoded['error_description'] ?? 'No description';
$this->message = $this->error . ': ' . $this->errorDescription;
}
}
$type = match ($code) {
400 => AppwriteException::USER_OAUTH2_BAD_REQUEST,
401 => AppwriteException::USER_OAUTH2_UNAUTHORIZED,
default => AppwriteException::USER_OAUTH2_PROVIDER_ERROR
};
parent::__construct($type, $this->message, $code, $previous);
}
/**
* Get the error parameter from the response.
*
* See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information.
*/
public function getError(): string
{
return $this->error;
}
/**
* Get the error_description parameter from the response.
*
* See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information.
*/
public function getErrorDescription(): string
{
return $this->errorDescription;
}
}
+389
View File
@@ -0,0 +1,389 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
class Firebase extends OAuth2
{
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/identitytoolkit',
'https://www.googleapis.com/auth/userinfo.profile'
];
/**
* @var array
*/
protected array $iamPermissions = [
// Database
'datastore.databases.get',
'datastore.databases.list',
'datastore.entities.get',
'datastore.entities.list',
'datastore.indexes.get',
'datastore.indexes.list',
// Generic Firebase permissions
'firebase.projects.get',
// Auth
'firebaseauth.configs.get',
'firebaseauth.configs.getHashConfig',
'firebaseauth.configs.getSecret',
'firebaseauth.users.get',
'identitytoolkit.tenants.get',
'identitytoolkit.tenants.list',
// IAM Assignment
'iam.serviceAccounts.list',
// Storage
'storage.buckets.get',
'storage.buckets.list',
'storage.objects.get',
'storage.objects.list'
];
/**
* @return string
*/
public function getName(): string
{
return 'firebase';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([
'access_type' => 'offline',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code',
'prompt' => 'consent',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
])
);
$this->tokens = \json_decode($response, true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
if ($user['verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
$response = $this->request(
'GET',
'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' . \urlencode($accessToken),
[],
);
$this->user = \json_decode($response, true);
}
return $this->user;
}
public function getProjects(string $accessToken): array
{
$projects = $this->request('GET', 'https://firebase.googleapis.com/v1beta1/projects', ['Authorization: Bearer ' . \urlencode($accessToken)]);
$projects = \json_decode($projects, true);
return $projects['results'];
}
/*
Be careful with the setIAMPolicy method, it will overwrite all existing policies
**/
public function assignIAMRole(string $accessToken, string $email, string $projectId, array $role)
{
// Get IAM Roles
$iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':getIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]);
$iamRoles = \json_decode($iamRoles, true);
$iamRoles['bindings'][] = [
'role' => $role['name'],
'members' => [
'serviceAccount:' . $email
]
];
// Set IAM Roles
$this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':setIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
], \json_encode([
'policy' => $iamRoles
]));
}
private function generateRandomString($length = 10): string
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
private function createCustomRole(string $accessToken, string $projectId): array
{
// Check if role already exists
try {
$role = $this->request('GET', 'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/appwriteMigrations', [
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
]);
$role = \json_decode($role, true);
return $role;
} catch (\Exception $e) {
if ($e->getCode() !== 404) {
throw $e;
}
}
// Create role if doesn't exist or isn't correct
$role = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/',
[
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
],
\json_encode(
[
'roleId' => 'appwriteMigrations',
'role' => [
'title' => 'Appwrite Migrations',
'description' => 'A helper role for Appwrite Migrations',
'includedPermissions' => $this->iamPermissions,
'stage' => 'GA'
]
]
)
);
return json_decode($role, true);
}
public function createServiceAccount(string $accessToken, string $projectId): array
{
// Create Service Account
$uid = $this->generateRandomString();
$response = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
],
\json_encode([
'accountId' => 'appwrite-' . $uid,
'serviceAccount' => [
'displayName' => 'Appwrite Migrations ' . $uid
]
])
);
$response = json_decode($response, true);
// Create and assign IAM Roles
$role = $this->createCustomRole($accessToken, $projectId);
\sleep(1); // Wait for IAM to propagate changes.
$this->assignIAMRole($accessToken, $response['email'], $projectId, $role);
// Create Service Account Key
$responseKey = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $response['email'] . '/keys',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$responseKey = json_decode($responseKey, true);
return json_decode(base64_decode($responseKey['privateKeyData']), true);
}
public function cleanupServiceAccounts(string $accessToken, string $projectId)
{
// List Service Accounts
$response = $this->request(
'GET',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$response = json_decode($response, true);
if (empty($response['accounts'])) {
return false;
}
foreach ($response['accounts'] as $account) {
if (strpos($account['email'], 'appwrite-') !== false) {
$this->request(
'DELETE',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $account['email'],
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
}
}
return true;
}
}
+40 -3
View File
@@ -158,6 +158,18 @@ class Github extends OAuth2
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserSlug(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['login'] ?? '';
}
/**
* @param string $accessToken
*
@@ -171,15 +183,40 @@ class Github extends OAuth2
$emails = $this->request('GET', 'https://api.github.com/user/emails', ['Authorization: token ' . \urlencode($accessToken)]);
$emails = \json_decode($emails, true);
$verifiedEmail = null;
$primaryEmail = null;
foreach ($emails as $email) {
if (isset($email['verified']) && $email['verified'] === true) {
$this->user['email'] = $email['email'];
$this->user['verified'] = $email['verified'];
break;
$verifiedEmail = $email;
if (isset($email['primary']) && $email['primary'] === true) {
$primaryEmail = $email;
}
}
}
if (!empty($primaryEmail)) {
$this->user['email'] = $primaryEmail['email'];
$this->user['verified'] = $primaryEmail['verified'];
} elseif (!empty($verifiedEmail)) {
$this->user['email'] = $verifiedEmail['email'];
$this->user['verified'] = $verifiedEmail['verified'];
}
}
return $this->user;
}
public function createRepository(string $accessToken, string $repositoryName, bool $private): array
{
$repository = $this->request('POST', 'https://api.github.com/user/repos', ['Authorization: token ' . \urlencode($accessToken)], \json_encode([
'name' => $repositoryName,
'private' => $private
]));
$repository = \json_decode($repository, true);
return $repository;
}
}
@@ -0,0 +1,102 @@
<?php
namespace Appwrite\Auth\Validator;
/**
* Validates user password string against their personal data
*/
class PersonalData extends Password
{
public function __construct(
protected ?string $userId = null,
protected ?string $email = null,
protected ?string $name = null,
protected ?string $phone = null,
protected bool $strict = false
) {
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Password must not include any personal data like your name, email, phone number, etc.';
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($password): bool
{
if (!parent::isValid($password)) {
return false;
}
if (!$this->strict) {
$password = strtolower($password);
$this->userId = strtolower($this->userId);
$this->email = strtolower($this->email);
$this->name = strtolower($this->name);
$this->phone = strtolower($this->phone);
}
if ($this->userId && strpos($password, $this->userId) !== false) {
return false;
}
if ($this->email && strpos($password, $this->email) !== false) {
return false;
}
if ($this->email && strpos($password, explode('@', $this->email)[0] ?? '') !== false) {
return false;
}
if ($this->name && strpos($password, $this->name) !== false) {
return false;
}
if ($this->phone && strpos($password, str_replace('+', '', $this->phone)) !== false) {
return false;
}
if ($this->phone && strpos($password, $this->phone) !== false) {
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_STRING;
}
}
-135
View File
@@ -1,135 +0,0 @@
<?php
namespace Appwrite\DSN;
class DSN
{
/**
* @var string
*/
protected string $scheme;
/**
* @var ?string
*/
protected ?string $user;
/**
* @var ?string
*/
protected ?string $password;
/**
* @var string
*/
protected string $host;
/**
* @var ?string
*/
protected ?string $port;
/**
* @var ?string
*/
protected ?string $database;
/**
* @var ?string
*/
protected ?string $query;
/**
* Construct
*
* Construct a new DSN object
*
* @param string $dsn
*/
public function __construct(string $dsn)
{
$parts = parse_url($dsn);
if (!$parts) {
throw new \InvalidArgumentException("Unable to parse DSN: $dsn");
}
$this->scheme = $parts['scheme'] ?? null;
$this->user = $parts['user'] ?? null;
$this->password = $parts['pass'] ?? null;
$this->host = $parts['host'] ?? null;
$this->port = $parts['port'] ?? null;
$this->database = $parts['path'] ?? null;
$this->query = $parts['query'] ?? null;
}
/**
* Return the scheme.
*
* @return string
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* Return the user.
*
* @return ?string
*/
public function getUser(): ?string
{
return $this->user;
}
/**
* Return the password.
*
* @return ?string
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* Return the host
*
* @return string
*/
public function getHost(): string
{
return $this->host;
}
/**
* Return the port
*
* @return ?string
*/
public function getPort(): ?string
{
return $this->port;
}
/**
* Return the database
*
* @return ?string
*/
public function getDatabase(): ?string
{
return ltrim($this->database, '/');
}
/**
* Return the query string
*
* @return ?string
*/
public function getQuery(): ?string
{
return $this->query;
}
}
+7 -3
View File
@@ -77,10 +77,14 @@ class Detector
*/
public function getDevice(): array
{
$deviceName = $this->getDetector()->getDeviceName();
$deviceBrand = $this->getDetector()->getBrandName();
$deviceModel = $this->getDetector()->getModel();
return [
'deviceName' => $this->getDetector()->getDeviceName(),
'deviceBrand' => $this->getDetector()->getBrandName(),
'deviceModel' => $this->getDetector()->getModel(),
'deviceName' => empty($deviceName) ? null : $deviceName,
'deviceBrand' => empty($deviceBrand) ? null : $deviceBrand,
'deviceModel' => empty($deviceModel) ? null : $deviceModel,
];
}
+16 -1
View File
@@ -10,12 +10,26 @@ class Build extends Event
protected string $type = '';
protected ?Document $resource = null;
protected ?Document $deployment = null;
protected ?Document $template = null;
public function __construct()
{
parent::__construct(Event::BUILDS_QUEUE_NAME, Event::BUILDS_CLASS_NAME);
}
/**
* Sets template for the build event.
*
* @param Document $template
* @return self
*/
public function setTemplate(Document $template): self
{
$this->template = $template;
return $this;
}
/**
* Sets resource document for the build event.
*
@@ -97,7 +111,8 @@ class Build extends Event
'project' => $this->project,
'resource' => $this->resource,
'deployment' => $this->deployment,
'type' => $this->type
'type' => $this->type,
'template' => $this->template
]);
}
}
+1 -2
View File
@@ -14,7 +14,6 @@ class Delete extends Event
protected ?string $datetime = null;
protected ?string $hourlyUsageRetentionDatetime = null;
public function __construct()
{
parent::__construct(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME);
@@ -142,7 +141,7 @@ class Delete extends Event
'resource' => $this->resource,
'resourceType' => $this->resourceType,
'datetime' => $this->datetime,
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime,
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime
]);
}
}
+34 -7
View File
@@ -23,6 +23,9 @@ class Event
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
public const USAGE_QUEUE_NAME = 'v1-usage';
public const USAGE_CLASS_NAME = 'UsageV1';
public const WEBHOOK_QUEUE_NAME = 'v1-webhooks';
public const WEBHOOK_CLASS_NAME = 'WebhooksV1';
@@ -35,6 +38,9 @@ class Event
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';
@@ -43,6 +49,7 @@ class Event
protected array $context = [];
protected ?Document $project = null;
protected ?Document $user = null;
protected bool $paused = false;
/**
* @param string $queue
@@ -116,9 +123,9 @@ class Event
/**
* Get project for this event.
*
* @return Document
* @return ?Document
*/
public function getProject(): Document
public function getProject(): ?Document
{
return $this->project;
}
@@ -137,11 +144,11 @@ class Event
}
/**
* Get project for this event.
* Get user responsible for triggering this event.
*
* @return Document
* @return ?Document
*/
public function getUser(): Document
public function getUser(): ?Document
{
return $this->user;
}
@@ -260,6 +267,10 @@ class Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
return Resque::enqueue($this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
@@ -337,8 +348,6 @@ class Event
default => false
};
return [
'type' => $type,
'resource' => $resource,
@@ -467,4 +476,22 @@ class Event
*/
return \array_values($events);
}
/**
* Get the value of paused
*/
public function isPaused(): bool
{
return $this->paused;
}
/**
* Set the value of paused
*/
public function setPaused(bool $paused): self
{
$this->paused = $paused;
return $this;
}
}
+76 -26
View File
@@ -2,20 +2,22 @@
namespace Appwrite\Event;
use DateTime;
use Resque;
use ResqueScheduler;
use Utopia\Database\Document;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
class Func extends Event
{
protected string $jwt = '';
protected string $type = '';
protected string $data = '';
protected string $body = '';
protected string $path = '';
protected string $method = '';
protected array $headers = [];
protected ?Document $function = null;
protected ?Document $execution = null;
public function __construct()
public function __construct(protected Connection $connection)
{
parent::__construct(Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME);
}
@@ -90,14 +92,53 @@ class Func extends Event
}
/**
* Sets custom data for the function event.
* Sets custom body for the function event.
*
* @param string $data
* @param string $body
* @return self
*/
public function setData(string $data): self
public function setBody(string $body): self
{
$this->data = $data;
$this->body = $body;
return $this;
}
/**
* Sets custom method for the function event.
*
* @param string $method
* @return self
*/
public function setMethod(string $method): self
{
$this->method = $method;
return $this;
}
/**
* Sets custom path for the function event.
*
* @param string $path
* @return self
*/
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
/**
* Sets custom headers for the function event.
*
* @param string $headers
* @return self
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
return $this;
}
@@ -143,7 +184,15 @@ class Func extends Event
*/
public function trigger(): string|bool
{
return Resque::enqueue($this->queue, $this->class, [
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
$events = $this->getEvent() ? Event::generateEvents($this->getEvent(), $this->getParams()) : null;
return $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'function' => $this->function,
@@ -151,28 +200,29 @@ class Func extends Event
'type' => $this->type,
'jwt' => $this->jwt,
'payload' => $this->payload,
'data' => $this->data
'events' => $events,
'body' => $this->body,
'path' => $this->path,
'headers' => $this->headers,
'method' => $this->method,
]);
}
/**
* Schedules the function event and schedules it in the functions worker queue.
* Generate a function event from a base event
*
* @param Event $event
*
* @return self
*
* @param \DateTime|int $at
* @return void
* @throws \Resque_Exception
* @throws \ResqueScheduler_InvalidTimestampException
*/
public function schedule(DateTime|int $at): void
public function from(Event $event): self
{
ResqueScheduler::enqueueAt($at, $this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'function' => $this->function,
'execution' => $this->execution,
'type' => $this->type,
'payload' => $this->payload,
'data' => $this->data
]);
$this->project = $event->getProject();
$this->user = $event->getUser();
$this->payload = $event->getPayload();
$this->event = $event->getEvent();
$this->params = $event->getParams();
return $this;
}
}
+203 -26
View File
@@ -8,10 +8,11 @@ use Utopia\Database\Document;
class Mail extends Event
{
protected string $recipient = '';
protected string $from = '';
protected string $name = '';
protected string $subject = '';
protected string $body = '';
protected array $smtp = [];
protected array $variables = [];
public function __construct()
{
@@ -32,7 +33,7 @@ class Mail extends Event
}
/**
* Returns set team for the mail event.
* Returns subject for the mail event.
*
* @return string
*/
@@ -64,29 +65,6 @@ class Mail extends Event
return $this->recipient;
}
/**
* Sets from for the mail event.
*
* @param string $from
* @return self
*/
public function setFrom(string $from): self
{
$this->from = $from;
return $this;
}
/**
* Returns from for mail event.
*
* @return string
*/
public function getFrom(): string
{
return $this->from;
}
/**
* Sets body for the mail event.
*
@@ -133,6 +111,204 @@ class Mail extends Event
return $this->name;
}
/**
* Set SMTP Host
*
* @param string $host
* @return self
*/
public function setSmtpHost(string $host): self
{
$this->smtp['host'] = $host;
return $this;
}
/**
* Set SMTP port
*
* @param int port
* @return self
*/
public function setSmtpPort(int $port): self
{
$this->smtp['port'] = $port;
return $this;
}
/**
* Set SMTP username
*
* @param string $username
* @return self
*/
public function setSmtpUsername(string $username): self
{
$this->smtp['username'] = $username;
return $this;
}
/**
* Set SMTP password
*
* @param string $password
* @return self
*/
public function setSmtpPassword(string $password): self
{
$this->smtp['password'] = $password;
return $this;
}
/**
* Set SMTP secure
*
* @param string $password
* @return self
*/
public function setSmtpSecure(string $secure): self
{
$this->smtp['secure'] = $secure;
return $this;
}
/**
* Set SMTP sender email
*
* @param string $senderEmail
* @return self
*/
public function setSmtpSenderEmail(string $senderEmail): self
{
$this->smtp['senderEmail'] = $senderEmail;
return $this;
}
/**
* Set SMTP sender name
*
* @param string $senderName
* @return self
*/
public function setSmtpSenderName(string $senderName): self
{
$this->smtp['senderName'] = $senderName;
return $this;
}
/**
* Set SMTP reply to
*
* @param string $replyTo
* @return self
*/
public function setSmtpReplyTo(string $replyTo): self
{
$this->smtp['replyTo'] = $replyTo;
return $this;
}
/**
* Get SMTP
*
* @return string
*/
public function getSmtpHost(): string
{
return $this->smtp['host'] ?? '';
}
/**
* Get SMTP port
*
* @return integer
*/
public function getSmtpPort(): int
{
return $this->smtp['port'] ?? 0;
}
/**
* Get SMTP username
*
* @return string
*/
public function getSmtpUsername(): string
{
return $this->smtp['username'] ?? '';
}
/**
* Get SMTP password
*
* @return string
*/
public function getSmtpPassword(): string
{
return $this->smtp['password'] ?? '';
}
/**
* Get SMTP secure
*
* @return string
*/
public function getSmtpSecure(): string
{
return $this->smtp['secure'] ?? '';
}
/**
* Get SMTP sender email
*
* @return string
*/
public function getSmtpSenderEmail(): string
{
return $this->smtp['senderEmail'] ?? '';
}
/**
* Get SMTP sender name
*
* @return string
*/
public function getSmtpSenderName(): string
{
return $this->smtp['senderName'] ?? '';
}
/**
* Get SMTP reply to
*
* @return string
*/
public function getSmtpReplyTo(): string
{
return $this->smtp['replyTo'] ?? '';
}
/**
* Get Email Variables
*
* @return array
*/
public function getVariables(): array
{
return $this->variables;
}
/**
* Set Email Variables
*
* @param array $variables
* @return self
*/
public function setVariables(array $variables): self
{
$this->variables = $variables;
return $this;
}
/**
* Executes the event and sends it to the mails worker.
*
@@ -142,11 +318,12 @@ class Mail extends Event
public function trigger(): string|bool
{
return Resque::enqueue($this->queue, $this->class, [
'from' => $this->from,
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'body' => $this->body,
'smtp' => $this->smtp,
'variables' => $this->variables,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Appwrite\Event;
use DateTime;
use Resque;
use ResqueScheduler;
use Utopia\Database\Document;
class Migration extends Event
{
protected string $type = '';
protected ?Document $migration = null;
public function __construct()
{
parent::__construct(Event::MIGRATIONS_QUEUE_NAME, Event::MIGRATIONS_CLASS_NAME);
}
/**
* Sets migration document for the migration event.
*
* @param Document $migration
* @return self
*/
public function setMigration(Document $migration): self
{
$this->migration = $migration;
return $this;
}
/**
* Returns set migration document for the function event.
*
* @return null|Document
*/
public function getMigration(): ?Document
{
return $this->migration;
}
/**
* Sets migration type for the migration event.
*
* @param string $type
*
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set migration type for the migration event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Executes the migration event and sends it to the migrations worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
return Resque::enqueue($this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration
]);
}
/**
* Schedules the migration event and schedules it in the migrations worker queue.
*
* @param \DateTime|int $at
* @return void
* @throws \Resque_Exception
* @throws \ResqueScheduler_InvalidTimestampException
*/
public function schedule(DateTime|int $at): void
{
ResqueScheduler::enqueueAt($at, $this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration
]);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Event;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Database\Document;
class Usage extends Event
{
protected array $metrics = [];
protected array $reduce = [];
public function __construct(protected Connection $connection)
{
parent::__construct(Event::USAGE_QUEUE_NAME, Event::USAGE_CLASS_NAME);
}
/**
* Add reduce.
*
* @param Document $document
* @return self
*/
public function addReduce(Document $document): self
{
$this->reduce[] = $document;
return $this;
}
/**
* Add metric.
*
* @param string $key
* @param int $value
* @return self
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
];
return $this;
}
/**
* Sends metrics to the usage worker.
*
* @return string|bool
*/
public function trigger(): string|bool
{
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->getProject(),
'reduce' => $this->reduce,
'metrics' => $this->metrics,
]);
}
}
+12 -1
View File
@@ -7,6 +7,11 @@ use Utopia\Validator;
class Event extends Validator
{
/**
* @var string
*/
protected string $message = 'Event is not valid.';
/**
* Get Description.
*
@@ -16,7 +21,7 @@ class Event extends Validator
*/
public function getDescription(): string
{
return 'Event is not valid.';
return $this->message;
}
/**
@@ -40,6 +45,12 @@ class Event extends Validator
* Identify all sections of the pattern.
*/
$type = $parts[0] ?? false;
if ($type == 'functions') {
$this->message = 'Triggering a function on a function event is not allowed.';
return false;
}
$resource = $parts[1] ?? false;
$hasSubResource = $count > 3 && ($events[$type]['$resource'] ?? false) && ($events[$type][$parts[2]]['$resource'] ?? false);
$hasSubSubResource = $count > 5 && $hasSubResource && ($events[$type][$parts[2]][$parts[4]]['$resource'] ?? false);
+40 -6
View File
@@ -32,6 +32,7 @@ class Exception extends \Exception
* - Platform
* - Domain
* - GraphQL
* - Migrations
*/
/** General */
@@ -51,7 +52,9 @@ class Exception extends \Exception
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
public const GENERAL_SERVER_ERROR = 'general_server_error';
public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
@@ -62,19 +65,25 @@ class Exception extends \Exception
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
public const USER_INVALID_CODE = 'user_invalid_code';
public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
public const USER_NOT_FOUND = 'user_not_found';
public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
public const USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
public const USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
public const USER_UNAUTHORIZED = 'user_unauthorized';
public const USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported';
public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
public const USER_PHONE_NOT_FOUND = 'user_phone_not_found';
public const USER_MISSING_ID = 'user_missing_id';
public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request';
public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized';
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
/** Teams */
public const TEAM_NOT_FOUND = 'team_not_found';
@@ -97,6 +106,7 @@ class Exception extends \Exception
public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found';
/** Storage */
public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists';
public const STORAGE_FILE_NOT_FOUND = 'storage_file_not_found';
public const STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found';
public const STORAGE_FILE_EMPTY = 'storage_file_empty';
@@ -107,10 +117,19 @@ class Exception extends \Exception
public const STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found';
public const STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range';
public const STORAGE_INVALID_RANGE = 'storage_invalid_range';
public const STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id';
/** VCS */
public const INSTALLATION_NOT_FOUND = 'installation_not_found';
public const PROVIDER_REPOSITORY_NOT_FOUND = 'provider_repository_not_found';
public const REPOSITORY_NOT_FOUND = 'repository_not_found';
public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict';
public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
/** 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';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
@@ -156,6 +175,7 @@ class Exception extends \Exception
public const INDEX_NOT_FOUND = 'index_not_found';
public const INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded';
public const INDEX_ALREADY_EXISTS = 'index_already_exists';
public const INDEX_INVALID = 'index_invalid';
/** Projects */
public const PROJECT_NOT_FOUND = 'project_not_found';
@@ -168,9 +188,23 @@ class Exception extends \Exception
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
public const PROJECT_KEY_EXPIRED = 'project_key_expired';
public const PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion';
/** Webhooks */
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
/** Router */
public const ROUTER_HOST_NOT_FOUND = 'router_host_not_found';
public const ROUTER_DOMAIN_NOT_CONFIGURED = 'router_domain_not_configured';
/** Proxy */
public const RULE_RESOURCE_NOT_FOUND = 'rule_resource_not_found';
public const RULE_NOT_FOUND = 'rule_not_found';
public const RULE_ALREADY_EXISTS = 'rule_already_exists';
public const RULE_VERIFICATION_FAILED = 'rule_verification_failed';
/** Keys */
public const KEY_NOT_FOUND = 'key_not_found';
@@ -181,17 +215,17 @@ class Exception extends \Exception
/** Platform */
public const PLATFORM_NOT_FOUND = 'platform_not_found';
/** Domain */
public const DOMAIN_NOT_FOUND = 'domain_not_found';
public const DOMAIN_ALREADY_EXISTS = 'domain_already_exists';
public const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
public const DOMAIN_TARGET_INVALID = 'domain_target_invalid';
/** GraphqQL */
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
/** Migrations */
public const MIGRATION_NOT_FOUND = 'migration_not_found';
public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
public const MIGRATION_IN_PROGRESS = 'migration_in_progress';
protected $type = '';
protected $errors = [];
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
{
-110
View File
@@ -1,110 +0,0 @@
<?php
namespace Appwrite\Extend;
use PDO as PDONative;
class PDO extends PDONative
{
/**
* @var PDONative
*/
protected $pdo;
/**
* @var mixed
*/
protected $dsn;
/**
* @var mixed
*/
protected $username;
/**
* @var mixed
*/
protected $passwd;
/**
* @var mixed
*/
protected $options;
/**
* Create A Proxy PDO Object
*/
public function __construct($dsn, $username = null, $passwd = null, $options = null)
{
$this->dsn = $dsn;
$this->username = $username;
$this->passwd = $passwd;
$this->options = $options;
$this->pdo = new PDONative($dsn, $username, $passwd, $options);
}
public function setAttribute($attribute, $value)
{
return $this->pdo->setAttribute($attribute, $value);
}
public function prepare($statement, $driver_options = null)
{
return new PDOStatement($this, $this->pdo->prepare($statement, []));
}
public function quote($string, $parameter_type = PDONative::PARAM_STR)
{
return $this->pdo->quote($string, $parameter_type);
}
public function beginTransaction()
{
try {
$result = $this->pdo->beginTransaction();
} catch (\Throwable $th) {
$this->pdo = $this->reconnect();
$result = $this->pdo->beginTransaction();
}
return $result;
}
public function rollBack()
{
try {
$result = $this->pdo->rollBack();
} catch (\Throwable $th) {
$this->pdo = $this->reconnect();
return false;
}
return $result;
}
public function commit()
{
try {
$result = $this->pdo->commit();
} catch (\Throwable $th) {
$this->pdo = $this->reconnect();
$result = $this->pdo->commit();
}
return $result;
}
public function reconnect(): PDONative
{
$this->pdo = new PDONative($this->dsn, $this->username, $this->passwd, $this->options);
echo '[PDO] MySQL connection restarted' . PHP_EOL;
// Connection settings
$this->pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays
$this->pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions
return $this->pdo;
}
}
-115
View File
@@ -1,115 +0,0 @@
<?php
namespace Appwrite\Extend;
use PDO as PDONative;
use PDOStatement as PDOStatementNative;
class PDOStatement extends PDOStatementNative
{
/**
* @var PDO
*/
protected $pdo;
/**
* Params
*/
protected $params = [];
/**
* Values
*/
protected $values = [];
/**
* Columns
*/
protected $columns = [];
/**
* @var PDOStatementNative
*/
protected $PDOStatement;
public function __construct(PDO &$pdo, PDOStatementNative $PDOStatement)
{
$this->pdo = &$pdo;
$this->PDOStatement = $PDOStatement;
}
public function bindValue($parameter, $value, $data_type = PDONative::PARAM_STR)
{
$this->values[$parameter] = ['value' => $value, 'data_type' => $data_type];
$result = $this->PDOStatement->bindValue($parameter, $value, $data_type);
return $result;
}
public function bindParam($parameter, &$variable, $data_type = PDONative::PARAM_STR, $length = null, $driver_options = null)
{
$this->params[$parameter] = ['value' => &$variable, 'data_type' => $data_type, 'length' => $length, 'driver_options' => $driver_options];
$result = $this->PDOStatement->bindParam($parameter, $variable, $data_type, $length, $driver_options);
return $result;
}
public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
{
$this->columns[$column] = ['param' => &$param, 'type' => $type, 'maxlen' => $maxlen, 'driverdata' => $driverdata];
$result = $this->PDOStatement->bindColumn($column, $param, $type, $maxlen, $driverdata);
return $result;
}
public function execute($input_parameters = null)
{
try {
$result = $this->PDOStatement->execute($input_parameters);
} catch (\Throwable $th) {
$this->pdo = $this->pdo->reconnect();
$this->PDOStatement = $this->pdo->prepare($this->PDOStatement->queryString, []);
foreach ($this->values as $key => $set) {
$this->PDOStatement->bindValue($key, $set['value'], $set['data_type']);
}
foreach ($this->params as $key => $set) {
$this->PDOStatement->bindParam($key, $set['variable'], $set['data_type'], $set['length'], $set['driver_options']);
}
foreach ($this->columns as $key => $set) {
$this->PDOStatement->bindColumn($key, $set['param'], $set['type'], $set['maxlen'], $set['driverdata']);
}
$result = $this->PDOStatement->execute($input_parameters);
}
return $result;
}
public function fetch($fetch_style = PDONative::FETCH_ASSOC, $cursor_orientation = PDONative::FETCH_ORI_NEXT, $cursor_offset = 0)
{
$result = $this->PDOStatement->fetch($fetch_style, $cursor_orientation, $cursor_offset);
return $result;
}
/**
* Fetch All
*
* @param int $fetch_style
* @param mixed $fetch_args
*
* @return array|false
*/
public function fetchAll(int $fetch_style = PDO::FETCH_BOTH, mixed ...$fetch_args)
{
$result = $this->PDOStatement->fetchAll();
return $result;
}
}
+1 -1
View File
@@ -269,7 +269,7 @@ class Resolvers
try {
$route = $utopia->match($request, fresh: true);
$utopia->execute($route, $request);
$utopia->execute($route, $request, $response);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
+6 -2
View File
@@ -252,16 +252,20 @@ class Mapper
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Utopia\Database\Validator\Permissions':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Appwrite\Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Roles':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
@@ -260,6 +260,11 @@ class Realtime extends Adapter
$channels[] = 'account.' . $parts[1];
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'rules':
$channels[] = 'console';
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
case 'teams':
if ($parts[2] === 'memberships') {
$permissionsChanged = $parts[4] ?? false;
@@ -325,6 +330,11 @@ class Realtime extends Adapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'migrations':
$channels[] = 'console';
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
}
+48 -39
View File
@@ -64,6 +64,8 @@ abstract class Migration
'1.3.6' => 'V18',
'1.3.7' => 'V18',
'1.3.8' => 'V18',
'1.4.0' => 'V19',
'1.4.1' => 'V19',
];
/**
@@ -76,7 +78,11 @@ abstract class Migration
Authorization::disable();
Authorization::setDefaultStatus(false);
$this->collections = array_merge([
$this->collections = Config::getParam('collections', []);
$projectCollections = $this->collections['projects'];
$this->collections['projects'] = array_merge([
'_metadata' => [
'$id' => ID::custom('_metadata'),
'$collection' => Database::METADATA
@@ -89,7 +95,7 @@ abstract class Migration
'$id' => ID::custom('abuse'),
'$collection' => Database::METADATA
]
], Config::getParam('collections', []));
], $projectCollections);
}
/**
@@ -105,8 +111,6 @@ abstract class Migration
{
$this->project = $project;
$this->projectDB = $projectDB;
$this->projectDB->setNamespace('_' . $this->project->getId());
$this->consoleDB = $consoleDB;
return $this;
@@ -132,7 +136,14 @@ abstract class Migration
*/
public function forEachDocument(callable $callback): void
{
foreach ($this->collections as $collection) {
$internalProjectId = $this->project->getInternalId();
$collections = match ($internalProjectId) {
'console' => $this->collections['console'],
default => $this->collections['projects'],
};
foreach ($collections as $collection) {
if ($collection['$collection'] !== Database::METADATA) {
continue;
}
@@ -149,12 +160,12 @@ abstract class Migration
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (is_null($new) || !self::hasDifference($new->getArrayCopy(), $old)) {
if (is_null($new) || $new->getArrayCopy() == $old) {
return;
}
try {
$new = $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
$this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
@@ -200,32 +211,6 @@ abstract class Migration
} while (!is_null($nextDocument));
}
/**
* Checks 2 arrays for differences.
*
* @param array $array1
* @param array $array2
* @return bool
*/
public static function hasDifference(array $array1, array $array2): bool
{
foreach ($array1 as $key => $value) {
if (is_array($value)) {
if (!isset($array2[$key]) || !is_array($array2[$key])) {
return true;
} else {
if (self::hasDifference($value, $array2[$key])) {
return true;
}
}
} elseif (!array_key_exists($key, $array2) || $array2[$key] !== $value) {
return true;
}
}
return false;
}
/**
* Creates colletion from the config collection.
*
@@ -238,10 +223,15 @@ abstract class Migration
{
$name ??= $id;
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
if (!$this->projectDB->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'), $name)) {
$attributes = [];
$indexes = [];
$collection = $this->collections[$id];
$collection = $this->collections[$collectionType][$id];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
@@ -287,10 +277,22 @@ abstract class Migration
public function createAttributeFromCollection(Database $database, string $collectionId, string $attributeId, string $from = null): void
{
$from ??= $collectionId;
$collection = Config::getParam('collections', [])[$from] ?? null;
if (is_null($collection)) {
throw new Exception("Collection {$collectionId} not found");
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
if ($from === 'files') {
$collectionType = 'buckets';
}
$collection = $this->collections[$collectionType][$from] ?? null;
if (is_null($collection)) {
throw new Exception("Collection {$from} not found");
}
$attributes = $collection['attributes'];
$attributeKey = array_search($attributeId, array_column($attributes, '$id'));
@@ -333,17 +335,24 @@ abstract class Migration
public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void
{
$from ??= $collectionId;
$collection = Config::getParam('collections', [])[$collectionId] ?? null;
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
$collection = $this->collections[$collectionType][$from] ?? null;
if (is_null($collection)) {
throw new Exception("Collection {$collectionId} not found");
}
$indexes = $collection['indexes'];
$indexKey = array_search($indexId, array_column($indexes, '$id'));
if ($indexKey === false) {
throw new Exception("Attribute {$indexId} not found");
throw new Exception("Index {$indexId} not found");
}
$index = $indexes[$indexKey];
+165
View File
@@ -70,6 +70,171 @@ class V17 extends Migration
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case 'builds':
try {
/**
* Create 'size' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'size');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'size' from {$id}: {$th->getMessage()}");
}
break;
case 'files':
try {
/**
* Update 'mimeType' attribute size (127->255)
*/
$this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false);
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'bucketInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'bucketInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
break;
case 'builds':
try {
/**
* Delete 'endTime' attribute (use startTime+duration if needed)
*/
$this->projectDB->deleteAttribute($id, 'endTime');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'endTime' from {$id}: {$th->getMessage()}");
}
try {
/**
* Rename 'outputPath' to 'path'
*/
$this->projectDB->renameAttribute($id, 'outputPath', 'path');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'path' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
break;
case 'stats':
try {
/**
* Delete 'type' attribute
*/
$this->projectDB->deleteAttribute($id, 'type');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
break;
case 'schedules':
try {
/**
* Create 'resourceInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
break;
case 'functions':
try {
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'scheduleInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}");
}
try {
/**
* Delete 'scheduleUpdatedAt' attribute
*/
$this->projectDB->deleteAttribute($id, 'scheduleUpdatedAt');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'scheduleUpdatedAt' from {$id}: {$th->getMessage()}");
}
break;
case 'deployments':
try {
/**
* Create 'resourceInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'buildInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'buildInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'buildInternalId' from {$id}: {$th->getMessage()}");
}
break;
case 'executions':
try {
/**
* Create 'functionInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'functionInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'functionInternalId' from {$id}: {$th->getMessage()}");
}
try {
/**
* Create 'deploymentInternalId' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
break;
default:
break;
}
+687
View File
@@ -0,0 +1,687 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
class V19 extends Migration
{
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables'] 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();
if ($this->project->getId() == 'console') {
Console::info('Migrating Domains');
$this->migrateDomains();
}
Console::info('Migrating Buckets');
$this->migrateBuckets();
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
Console::log('Cleaning Up Collections');
$this->cleanCollections();
}
protected function migrateDomains(): void
{
foreach ($this->documentsIterator('domains') as $domain) {
$status = 'created';
if ($domain->getAttribute('verification', false)) {
$status = 'verified';
}
$ruleDocument = new Document([
'projectId' => $domain->getAttribute('projectId'),
'projectInternalId' => $domain->getAttribute('projectInternalId'),
'domain' => $domain->getAttribute('domain'),
'resourceType' => 'api',
'resourceInternalId' => '',
'resourceId' => '',
'status' => $status,
'certificateId' => $domain->getAttribute('certificateId'),
]);
try {
$this->consoleDB->createDocument('rules', $ruleDocument);
} catch (\Throwable $th) {
Console::warning("Error migrating domain {$domain->getAttribute('domain')}: {$th->getMessage()}");
}
}
}
/**
* Migrating all Bucket tables.
*
* @return void
* @throws \Exception
* @throws \PDOException
*/
protected function migrateBuckets(): void
{
foreach ($this->documentsIterator('buckets') as $bucket) {
$id = "bucket_{$bucket->getInternalId()}";
Console::log("Migrating Bucket {$id} {$bucket->getId()} ({$bucket->getAttribute('name')})");
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'bucketInternalId', 'files');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'bucketInternalId' from {$id}: {$th->getMessage()}");
}
}
}
/**
* Migrate all Collections.
*
* @return void
* @throws \Throwable
* @throws Exception
*/
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'];
if ($id === 'schedules' && $internalProjectId === 'console') {
continue;
}
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
switch ($id) {
case '_metadata':
$this->createCollection('identities');
$this->createCollection('migrations');
// Holding off on this until a future release
// $this->createCollection('statsLogger');
break;
case 'attributes':
case 'indexes':
try {
$this->projectDB->updateAttribute($id, 'databaseInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'databaseInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'collectionInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'collectionInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'error');
} catch (\Throwable $th) {
Console::warning("'error' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'buckets':
// Recreate indexes so they're the right size
$indexesToDelete = [
'_key_name',
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$indexesToCreate = [
...$indexesToDelete
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'builds':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'logs');
} catch (\Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'certificates':
try {
$this->projectDB->renameAttribute($id, 'log', 'logs');
} catch (\Throwable $th) {
Console::warning("'errors' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'logs', size: 1000000);
} catch (\Throwable $th) {
Console::warning("'errors' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'databases':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
} catch (\Throwable $th) {
Console::warning("'enabled' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'deployments':
$attributesToCreate = [
'resourceInternalId',
'buildInternalId',
'type',
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("$attribute from {$id}: {$th->getMessage()}");
}
}
// Recreate indexes so they're the right size
$indexesToDelete = [
'_key_entrypoint',
'_key_resource',
'_key_resource_type',
'_key_buildId',
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$indexesToCreate = [
'_key_resource',
'_key_resource_type',
'_key_buildId',
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'executions':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'functionInternalId');
} catch (\Throwable $th) {
Console::warning("'functionInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'deploymentInternalId');
} catch (\Throwable $th) {
Console::warning("'deploymentInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'stderr', 'errors');
} catch (\Throwable $th) {
Console::warning("'errors' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'stdout', 'logs');
} catch (\Throwable $th) {
Console::warning("'logs' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'statusCode', 'responseStatusCode');
} catch (\Throwable $th) {
Console::warning("'responseStatusCode' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'files':
// Recreate indexes so they're the right size
$indexesToDelete = [
'_key_name',
'_key_signature',
'_key_mimeType',
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$indexesToCreate = $indexesToDelete;
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'functions':
$attributesToCreate = [
'live',
'installationId',
'installationInternalId',
'providerRepositoryId',
'repositoryId',
'repositoryInternalId',
'providerBranch',
'providerRootDirectory',
'providerSilentMode',
'logging',
'deploymentInternalId',
'scheduleInternalId',
'scheduleId',
'version',
'entrypoint',
'commands',
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
}
}
// Recreate indexes so they're the right size
$indexesToDelete = [
'_key_name',
'_key_runtime',
'_key_deployment',
];
foreach ($indexesToDelete as $index) {
try {
$this->projectDB->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$indexesToCreate = [
...$indexesToDelete,
'_key_installationId',
'_key_installationInternalId',
'_key_providerRepositoryId',
'_key_repositoryId',
'_key_repositoryInternalId',
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'memberships':
try {
$this->projectDB->updateAttribute($id, 'teamInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'teamInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
// Intentional fall through to update memberships.userInternalId
case 'sessions':
case 'tokens':
try {
$this->projectDB->updateAttribute($id, 'userInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'domains':
case 'keys':
case 'platforms':
case 'webhooks':
try {
$this->projectDB->updateAttribute($id, 'projectInternalId', required: true);
} catch (\Throwable $th) {
Console::warning("'projectInternalId' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'projects':
$attributesToCreate = [
'database',
'smtp',
'templates',
];
foreach ($attributesToCreate as $attribute) {
try {
$this->createAttributeFromCollection($this->projectDB, $id, $attribute);
} catch (\Throwable $th) {
Console::warning("'$attribute' from {$id}: {$th->getMessage()}");
Console::warning($th->getTraceAsString());
}
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'stats':
try {
$this->projectDB->updateAttribute($id, 'value', signed: true);
} catch (\Throwable $th) {
Console::warning("'value' from {$id}: {$th->getMessage()}");
}
// Holding off on these until a future release
// try {
// $this->projectDB->deleteAttribute($id, 'type');
// $this->projectDB->deleteCachedCollection($id);
// } catch (\Throwable $th) {
// Console::warning("'type' from {$id}: {$th->getMessage()}");
// }
// try {
// $this->projectDB->deleteIndex($id, '_key_metric_period_time');
// $this->projectDB->deleteCachedCollection($id);
// } catch (\Throwable $th) {
// Console::warning("'_key_metric_period_time' from {$id}: {$th->getMessage()}");
// }
// try {
// $this->createIndexFromCollection($this->projectDB, $id, '_key_metric_period_time');
// $this->projectDB->deleteCachedCollection($id);
// } catch (\Throwable $th) {
// Console::warning("'_key_metric_period_time' from {$id}: {$th->getMessage()}");
// }
$this->projectDB->deleteCachedCollection($id);
break;
case 'users':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'labels');
} catch (\Throwable $th) {
Console::warning("'labels' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
} catch (\Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttribute($id, 'search', filters: ['userSearch']);
} catch (\Throwable $th) {
Console::warning("'search' from {$id}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $id, '_key_accessedAt');
} catch (\Throwable $th) {
Console::warning("'_key_accessedAt' from {$id}: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection($id);
break;
case 'variables':
try {
$this->projectDB->deleteIndex($id, '_key_function');
} catch (\Throwable $th) {
Console::warning("'_key_function' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->deleteIndex($id, '_key_uniqueKey');
} catch (\Throwable $th) {
Console::warning("'_key_uniqueKey' from {$id}: {$th->getMessage()}");
}
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceType');
} catch (\Throwable $th) {
Console::warning("'resourceType' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'functionInternalId', 'resourceInternalId');
} catch (\Throwable $th) {
Console::warning("'resourceInternalId' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->renameAttribute($id, 'functionId', 'resourceId');
} catch (\Throwable $th) {
Console::warning("'resourceId' from {$id}: {$th->getMessage()}");
}
$indexesToCreate = [
'_key_resourceInternalId',
'_key_resourceId_resourceType',
'_key_resourceType',
'_key_uniqueKey',
];
foreach ($indexesToCreate as $index) {
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
}
$this->projectDB->deleteCachedCollection($id);
break;
default:
break;
}
usleep(50000);
}
}
/**
* Fix run on each document
*
* @param Document $document
* @return Document
*/
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'attributes':
case 'indexes':
$status = $document->getAttribute('status', '');
if ($status === 'failed') {
$document->setAttribute('error', 'Unknown problem');
}
break;
case 'builds':
$deploymentId = $document->getAttribute('deploymentId');
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
$stdout = $document->getAttribute('stdout', '');
$stderr = $document->getAttribute('stderr', '');
$document->setAttribute('logs', $stdout . PHP_EOL . $stderr);
break;
case 'databases':
$document->setAttribute('enabled', true);
break;
case 'deployments':
$resourceId = $document->getAttribute('resourceId');
$function = $this->projectDB->getDocument('functions', $resourceId);
$document->setAttribute('resourceInternalId', $function->getInternalId());
$buildId = $document->getAttribute('buildId');
if (!empty($buildId)) {
$build = $this->projectDB->getDocument('builds', $buildId);
$document->setAttribute('buildInternalId', $build->getInternalId());
}
$document->setAttribute('type', 'manual');
break;
case 'executions':
$functionId = $document->getAttribute('functionId');
$function = $this->projectDB->getDocument('functions', $functionId);
$document->setAttribute('functionInternalId', $function->getInternalId());
$deploymentId = $document->getAttribute('deploymentId');
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
break;
case 'functions':
$document->setAttribute('live', true);
$document->setAttribute('logging', true);
$document->setAttribute('version', 'v2');
$deploymentId = $document->getAttribute('deployment');
if (!empty($deploymentId)) {
$deployment = $this->projectDB->getDocument('deployments', $deploymentId);
$document->setAttribute('deploymentInternalId', $deployment->getInternalId());
$document->setAttribute('entrypoint', $deployment->getAttribute('entrypoint'));
}
$schedule = $this->consoleDB->createDocument('schedules', new Document([
'region' => App::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'resourceType' => 'function',
'resourceId' => $document->getId(),
'resourceInternalId' => $document->getInternalId(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $this->project->getId(),
'schedule' => $document->getAttribute('schedule'),
'active' => !empty($document->getAttribute('schedule')) && !empty($document->getAttribute('deployment')),
]));
$document->setAttribute('scheduleId', $schedule->getId());
$document->setAttribute('scheduleInternalId', $schedule->getInternalId());
break;
case 'projects':
$document->setAttribute('version', '1.4.0');
$databases = Config::getParam('pools-database', []);
$database = $databases[0];
$document->setAttribute('database', $database);
$document->setAttribute('smtp', []);
$document->setAttribute('templates', []);
break;
default:
break;
}
return $document;
}
private function cleanCollections(): void
{
try {
$this->projectDB->deleteAttribute('projects', 'domains');
} catch (\Throwable $th) {
Console::warning("'domains' from projects: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection('projects');
try {
$this->projectDB->deleteAttribute('functions', 'schedule');
} catch (\Throwable $th) {
Console::warning("'schedule' from functions: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection('functions');
try {
$this->projectDB->deleteAttribute('builds', 'stderr');
} catch (\Throwable $th) {
Console::warning("'stderr' from builds: {$th->getMessage()}");
}
try {
$this->projectDB->deleteAttribute('builds', 'stdout');
} catch (\Throwable $th) {
Console::warning("'stdout' from builds: {$th->getMessage()}");
}
$this->projectDB->deleteCachedCollection('builds');
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform;
use Appwrite\Platform\Services\Tasks;
use Utopia\Platform\Platform;
class Appwrite extends Platform
{
public function __construct()
{
$this->addService('tasks', new Tasks());
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Services;
use Utopia\Platform\Service;
use Appwrite\Platform\Tasks\Doctor;
use Appwrite\Platform\Tasks\Install;
use Appwrite\Platform\Tasks\Maintenance;
use Appwrite\Platform\Tasks\Migrate;
use Appwrite\Platform\Tasks\Schedule;
use Appwrite\Platform\Tasks\PatchCreateMissingSchedules;
use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\Platform\Tasks\SSL;
use Appwrite\Platform\Tasks\Hamster;
use Appwrite\Platform\Tasks\PatchDeleteScheduleUpdatedAtAttribute;
use Appwrite\Platform\Tasks\ClearCardCache;
use Appwrite\Platform\Tasks\Usage;
use Appwrite\Platform\Tasks\Vars;
use Appwrite\Platform\Tasks\Version;
use Appwrite\Platform\Tasks\VolumeSync;
use Appwrite\Platform\Tasks\CalcUsersStats;
use Appwrite\Platform\Tasks\CalcTierStats;
use Appwrite\Platform\Tasks\PatchDeleteProjectCollections;
use Appwrite\Platform\Tasks\Upgrade;
class Tasks extends Service
{
public function __construct()
{
$this->type = self::TYPE_CLI;
$this
->addAction(Version::getName(), new Version())
->addAction(Usage::getName(), new Usage())
->addAction(Vars::getName(), new Vars())
->addAction(SSL::getName(), new SSL())
->addAction(Hamster::getName(), new Hamster())
->addAction(Doctor::getName(), new Doctor())
->addAction(Install::getName(), new Install())
->addAction(Upgrade::getName(), new Upgrade())
->addAction(Maintenance::getName(), new Maintenance())
->addAction(PatchCreateMissingSchedules::getName(), new PatchCreateMissingSchedules())
->addAction(ClearCardCache::getName(), new ClearCardCache())
->addAction(PatchDeleteScheduleUpdatedAtAttribute::getName(), new PatchDeleteScheduleUpdatedAtAttribute())
->addAction(Schedule::getName(), new Schedule())
->addAction(Migrate::getName(), new Migrate())
->addAction(SDKs::getName(), new SDKs())
->addAction(VolumeSync::getName(), new VolumeSync())
->addAction(Specs::getName(), new Specs())
->addAction(CalcUsersStats::getName(), new CalcUsersStats())
->addAction(CalcTierStats::getName(), new CalcTierStats())
->addAction(PatchDeleteProjectCollections::getName(), new PatchDeleteProjectCollections())
;
}
}
@@ -0,0 +1,351 @@
<?php
namespace Appwrite\Platform\Tasks;
use Exception;
use League\Csv\CannotInsertRecord;
use Utopia\App;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use League\Csv\Writer;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
class CalcTierStats extends Action
{
/*
* Csv cols headers
*/
private array $columns = [
'Project ID',
'Organization ID',
'Organization Members',
'Teams',
'Users',
'Requests',
'Bandwidth',
'Domains',
'Api keys',
'Webhooks',
'Platforms',
'Buckets',
'Files',
'Storage (bytes)',
'Max File Size (bytes)',
'Databases',
'Functions',
'Deployments',
'Executions',
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
private array $usageStats = [
'project.$all.network.requests' => 'Requests',
'project.$all.network.bandwidth' => 'Bandwidth',
];
public static function getName(): string
{
return 'calc-tier-stats';
}
public function __construct()
{
$this
->desc('Get stats for projects')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->inject('register')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) {
$this->action($pools, $cache, $dbForConsole, $register);
});
}
/**
* @throws \Utopia\Exception
* @throws CannotInsertRecord
*/
public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void
{
//docker compose exec -t appwrite calc-tier-stats
Console::title('Cloud free tier stats calculation V1');
Console::success(APP_NAME . ' cloud free tier stats calculation has started');
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
/** CSV stuff */
$this->date = date('Y-m-d');
$this->path = "{$this->directory}/tier_stats_{$this->date}.csv";
$csv = Writer::createFromPath($this->path, 'w');
$csv->insertOne($this->columns);
/** Database connections */
$totalProjects = $dbForConsole->count('projects');
Console::success("Found a total of: {$totalProjects} projects");
$projects = [$console];
$count = 0;
$limit = 30;
$sum = 30;
$offset = 0;
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
continue;
}
Console::info("Getting stats for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
/** Get Project ID */
$stats['Project ID'] = $project->getId();
$stats['Organization ID'] = $project->getAttribute('teamId', null);
/** Get Total Members */
$teamInternalId = $project->getAttribute('teamInternalId', null);
if ($teamInternalId) {
$stats['Organization Members'] = $dbForConsole->count('memberships', [
Query::equal('teamInternalId', [$teamInternalId])
]);
} else {
$stats['Organization Members'] = 0;
}
/** Get Total internal Teams */
try {
$stats['Teams'] = $dbForProject->count('teams', []);
} catch (\Throwable) {
$stats['Teams'] = 0;
}
/** Get Total users */
try {
$stats['Users'] = $dbForProject->count('users', []);
} catch (\Throwable) {
$stats['Users'] = 0;
}
/** Get Usage stats */
$range = '90d';
$periods = [
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$tmp = [];
$metrics = $this->usageStats;
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$tmp) {
foreach ($metrics as $metric => $name) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$tmp[$metric] = [];
foreach ($requestDocs as $requestDoc) {
if (empty($requestDoc)) {
continue;
}
$tmp[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$tmp[$metric] = array_reverse($tmp[$metric]);
$tmp[$metric] = array_sum(array_column($tmp[$metric], 'value'));
}
});
foreach ($tmp as $key => $value) {
$stats[$metrics[$key]] = $value;
}
try {
/** Get Domains */
$stats['Domains'] = $dbForConsole->count('domains', [
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
} catch (\Throwable) {
$stats['Domains'] = 0;
}
try {
/** Get Api keys */
$stats['Api keys'] = $dbForConsole->count('keys', [
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
} catch (\Throwable) {
$stats['Api keys'] = 0;
}
try {
/** Get Webhooks */
$stats['Webhooks'] = $dbForConsole->count('webhooks', [
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
} catch (\Throwable) {
$stats['Webhooks'] = 0;
}
try {
/** Get Platforms */
$stats['Platforms'] = $dbForConsole->count('platforms', [
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
} catch (\Throwable) {
$stats['Platforms'] = 0;
}
/** Get Files & Buckets */
$filesCount = 0;
$filesSum = 0;
$maxFileSize = 0;
$counter = 0;
try {
$buckets = $dbForProject->find('buckets', []);
foreach ($buckets as $bucket) {
$file = $dbForProject->findOne('bucket_' . $bucket->getInternalId(), [Query::orderDesc('sizeOriginal'),]);
if (empty($file)) {
continue;
}
$filesSum += $dbForProject->sum('bucket_' . $bucket->getInternalId(), 'sizeOriginal', [], 0);
$filesCount += $dbForProject->count('bucket_' . $bucket->getInternalId(), []);
if ($file->getAttribute('sizeOriginal') > $maxFileSize) {
$maxFileSize = $file->getAttribute('sizeOriginal');
}
$counter++;
}
} catch (\Throwable) {
;
}
$stats['Buckets'] = $counter;
$stats['Files'] = $filesCount;
$stats['Storage (bytes)'] = $filesSum;
$stats['Max File Size (bytes)'] = $maxFileSize;
try {
/** Get Total Functions */
$stats['Databases'] = $dbForProject->count('databases', []);
} catch (\Throwable) {
$stats['Databases'] = 0;
}
/** Get Total Functions */
try {
$stats['Functions'] = $dbForProject->count('functions', []);
} catch (\Throwable) {
$stats['Functions'] = 0;
}
/** Get Total Deployments */
try {
$stats['Deployments'] = $dbForProject->count('deployments', []);
} catch (\Throwable) {
$stats['Deployments'] = 0;
}
/** Get Total Executions */
try {
$stats['Executions'] = $dbForProject->count('executions', []);
} catch (\Throwable) {
$stats['Executions'] = 0;
}
$csv->insertOne(array_values($stats));
} catch (\Throwable $th) {
Console::error('Failed on project ("' . $project->getId() . '") version with error on File: ' . $th->getFile() . ' line no: ' . $th->getline() . ' with message: ' . $th->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [
Query::limit($limit),
Query::offset($offset),
]);
$offset = $offset + $limit;
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects...');
$pools
->get('console')
->reclaim();
/** @var PHPMailer $mail */
$mail = $register->get('smtp');
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
try {
/** Addresses */
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster');
$recipients = explode(',', App::getEnv('_APP_USERS_STATS_RECIPIENTS', ''));
foreach ($recipients as $recipient) {
$mail->addAddress($recipient);
}
/** Attachments */
$mail->addAttachment($this->path);
/** Content */
$mail->Subject = "Cloud Report for {$this->date}";
$mail->Body = "Please find the daily cloud report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
}
@@ -0,0 +1,176 @@
<?php
namespace Appwrite\Platform\Tasks;
use Exception;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use League\Csv\Writer;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
class CalcUsersStats extends Action
{
private array $columns = [
'Project ID',
'Project Name',
'Team ID',
'Team name',
'Users'
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
public static function getName(): string
{
return 'calc-users-stats';
}
public function __construct()
{
$this
->desc('Get stats for projects')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->inject('register')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) {
$this->action($pools, $cache, $dbForConsole, $register);
});
}
public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void
{
//docker compose exec -t appwrite calc-users-stats
Console::title('Cloud Users calculation V1');
Console::success(APP_NAME . ' cloud Users calculation has started');
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
/** CSV stuff */
$this->date = date('Y-m-d');
$this->path = "{$this->directory}/users_stats_{$this->date}.csv";
$csv = Writer::createFromPath($this->path, 'w');
$csv->insertOne($this->columns);
/** Database connections */
$totalProjects = $dbForConsole->count('projects');
Console::success("Found a total of: {$totalProjects} projects");
$projects = [$console];
$count = 0;
$limit = 30;
$sum = 30;
$offset = 0;
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
continue;
}
Console::info("Getting stats for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
/** Get Project ID */
$stats['Project ID'] = $project->getId();
/** Get Project Name */
$stats['Project Name'] = $project->getAttribute('name');
/** Get Team Name and Id */
$teamId = $project->getAttribute('teamId', null);
$teamName = null;
if ($teamId) {
$team = $dbForConsole->getDocument('teams', $teamId);
$teamName = $team->getAttribute('name');
}
$stats['Team ID'] = $teamId;
$stats['Team name'] = $teamName;
/** Get Total Users */
$stats['users'] = $dbForProject->count('users', []);
$csv->insertOne(array_values($stats));
} catch (\Throwable $th) {
Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [
Query::limit($limit),
Query::offset($offset),
]);
$offset = $offset + $limit;
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects...');
$pools
->get('console')
->reclaim();
/** @var PHPMailer $mail */
$mail = $register->get('smtp');
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
try {
/** Addresses */
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster');
$recipients = explode(',', App::getEnv('_APP_USERS_STATS_RECIPIENTS', ''));
foreach ($recipients as $recipient) {
$mail->addAddress($recipient);
}
/** Attachments */
$mail->addAttachment($this->path);
/** Content */
$mail->Subject = "Cloud Report for {$this->date}";
$mail->Body = "Please find the daily cloud report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
}
@@ -0,0 +1,62 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\Query;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
class ClearCardCache extends Action
{
public static function getName(): string
{
return 'clear-card-cache';
}
public function __construct()
{
$this
->desc('Deletes card cache for specific user')
->param('userId', '', new UID(), 'User UID.', false)
->inject('dbForConsole')
->callback(fn (string $userId, Database $dbForConsole) => $this->action($userId, $dbForConsole));
}
public function action(string $userId, Database $dbForConsole): void
{
Authorization::disable();
Authorization::setDefaultStatus(false);
Console::title('ClearCardCache V1');
Console::success(APP_NAME . ' ClearCardCache v1 has started');
$resources = ['card/' . $userId, 'card-back/' . $userId, 'card-og/' . $userId];
$caches = Authorization::skip(fn () => $dbForConsole->find('cache', [
Query::equal('resource', $resources),
Query::limit(100)
]));
$count = \count($caches);
Console::info("Going to delete {$count} cache records in 10 seconds...");
\sleep(10);
foreach ($caches as $cache) {
$key = $cache->getId();
$cacheFolder = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-console')
);
$cacheFolder->purge($key);
Authorization::skip(fn () => $dbForConsole->deleteDocument('cache', $cache->getId()));
}
Console::success(APP_NAME . ' ClearCardCache v1 has finished');
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\App;
use Utopia\CLI\Console;
use Appwrite\ClamAV\Network;
use Utopia\Logger\Logger;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
class Doctor extends Action
{
public static function getName(): string
{
return 'doctor';
}
public function __construct()
{
$this
->desc('Validate server health')
->inject('register')
->callback(fn (Registry $register) => $this->action($register));
}
public function action(Registry $register): void
{
Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __
/ _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \
/ \ ) __/ ) __/\ /\ / ) / )( )( ) _) _ )(( O )
\_/\_/(__) (__) (_/\_)(__\_)(__) (__) (____)(_)(__)\__/ ");
Console::log("\n" . '👩‍⚕️ Running ' . APP_NAME . ' Doctor for version ' . App::getEnv('_APP_VERSION', 'UNKNOWN') . ' ...' . "\n");
Console::log('[Settings]');
$domain = new Domain(App::getEnv('_APP_DOMAIN'));
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 Hostname has no public suffix (' . $domain->get() . ')');
} else {
Console::log('🟢 Hostname has a public suffix (' . $domain->get() . ')');
}
$domain = new Domain(App::getEnv('_APP_DOMAIN_TARGET'));
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 CNAME target has no public suffix (' . $domain->get() . ')');
} else {
Console::log('🟢 CNAME target has a public suffix (' . $domain->get() . ')');
}
if (App::getEnv('_APP_OPENSSL_KEY_V1') === 'your-secret-key' || empty(App::getEnv('_APP_OPENSSL_KEY_V1'))) {
Console::log('🔴 Not using a unique secret key for encryption');
} else {
Console::log('🟢 Using a unique secret key for encryption');
}
if (App::getEnv('_APP_ENV', 'development') !== 'production') {
Console::log('🔴 App environment is set for development');
} else {
Console::log('🟢 App environment is set for production');
}
if ('enabled' !== App::getEnv('_APP_OPTIONS_ABUSE', 'disabled')) {
Console::log('🔴 Abuse protection is disabled');
} else {
Console::log('🟢 Abuse protection is enabled');
}
$authWhitelistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', null);
$authWhitelistEmails = App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null);
$authWhitelistIPs = App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null);
if (
empty($authWhitelistRoot)
&& empty($authWhitelistEmails)
&& empty($authWhitelistIPs)
) {
Console::log('🔴 Console access limits are disabled');
} else {
Console::log('🟢 Console access limits are enabled');
}
if ('enabled' !== App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled')) {
Console::log('🔴 HTTPS force option is disabled');
} else {
Console::log('🟢 HTTPS force option is enabled');
}
$providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
if (empty($providerName) || empty($providerConfig) || !Logger::hasProvider($providerName)) {
Console::log('🔴 Logging adapter is disabled');
} else {
Console::log('🟢 Logging adapter is enabled (' . $providerName . ')');
}
\sleep(0.2);
try {
Console::log("\n" . '[Connectivity]');
} catch (\Throwable $th) {
//throw $th;
}
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
$configs = [
'Console.DB' => Config::getParam('pools-console'),
'Projects.DB' => Config::getParam('pools-database'),
];
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
$adapter = $pools->get($database)->pop()->getResource();
if ($adapter->ping()) {
Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected');
} else {
Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected');
}
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected');
}
}
}
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
$configs = [
'Cache' => Config::getParam('pools-cache'),
'Queue' => Config::getParam('pools-queue'),
'PubSub' => Config::getParam('pools-pubsub'),
];
foreach ($configs as $key => $config) {
foreach ($config as $pool) {
try {
$adapter = $pools->get($pool)->pop()->getResource();
if ($adapter->ping()) {
Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected');
} else {
Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected');
}
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected');
}
}
}
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled
try {
$antivirus = new Network(
App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
if ((@$antivirus->ping())) {
Console::success('🟢 ' . str_pad("Antivirus", 50, '.') . 'connected');
} else {
Console::error('🔴 ' . str_pad("Antivirus", 47, '.') . 'disconnected');
}
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("Antivirus", 47, '.') . 'disconnected');
}
}
try {
$mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */
$mail->addAddress('demo@example.com', 'Example.com');
$mail->Subject = 'Test SMTP Connection';
$mail->Body = 'Hello World';
$mail->AltBody = 'Hello World';
$mail->send();
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
} catch (\Throwable $th) {
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
}
\sleep(0.2);
Console::log('');
Console::log('[Volumes]');
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES
] as $key => $volume
) {
$device = new Local($volume);
if (\is_readable($device->getRoot())) {
Console::success('🟢 ' . $key . ' Volume is readable');
} else {
Console::error('🔴 ' . $key . ' Volume is unreadable');
}
if (\is_writable($device->getRoot())) {
Console::success('🟢 ' . $key . ' Volume is writeable');
} else {
Console::error('🔴 ' . $key . ' Volume is unwriteable');
}
}
\sleep(0.2);
Console::log('');
Console::log('[Disk Space]');
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES
] as $key => $volume
) {
$device = new Local($volume);
$percentage = (($device->getPartitionTotalSpace() - $device->getPartitionFreeSpace())
/ $device->getPartitionTotalSpace()) * 100;
$message = $key . ' Volume has ' . Storage::human($device->getPartitionFreeSpace()) . ' free space (' . \round($percentage, 2) . '% used)';
if ($percentage < 80) {
Console::success('🟢 ' . $message);
} else {
Console::error('🔴 ' . $message);
}
}
try {
if (App::isProduction()) {
Console::log('');
$version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost') . '/v1/health/version'), true);
if ($version && isset($version['version'])) {
if (\version_compare($version['version'], App::getEnv('_APP_VERSION', 'UNKNOWN')) === 0) {
Console::info('You are running the latest version of ' . APP_NAME . '! 🥳');
} else {
Console::info('A new version (' . $version['version'] . ') is available! 🥳' . "\n");
}
} else {
Console::error('Failed to check for a newer version' . "\n");
}
}
} catch (\Throwable $th) {
Console::error('Failed to check for a newer version' . "\n");
}
}
}
+444
View File
@@ -0,0 +1,444 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Network\Validator\Origin;
use Exception;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Analytics\Adapter\Mixpanel;
use Utopia\Analytics\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Pools\Group;
class Hamster extends Action
{
private array $metrics = [
'usage_files' => 'files.$all.count.total',
'usage_buckets' => 'buckets.$all.count.total',
'usage_databases' => 'databases.$all.count.total',
'usage_documents' => 'documents.$all.count.total',
'usage_collections' => 'collections.$all.count.total',
'usage_storage' => 'project.$all.storage.size',
'usage_requests' => 'project.$all.network.requests',
'usage_bandwidth' => 'project.$all.network.bandwidth',
'usage_users' => 'users.$all.count.total',
'usage_sessions' => 'sessions.email.requests.create',
'usage_executions' => 'executions.$all.compute.total',
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
protected Mixpanel $mixpanel;
public static function getName(): string
{
return 'hamster';
}
public function __construct()
{
$this->mixpanel = new Mixpanel(App::getEnv('_APP_MIXPANEL_TOKEN', ''));
$this
->desc('Get stats for projects')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole) {
$this->action($pools, $cache, $dbForConsole);
});
}
private function getStatsPerProject(Group $pools, Cache $cache, Database $dbForConsole)
{
$this->calculateByGroup('projects', $dbForConsole, function (Database $dbForConsole, Document $project) use ($pools, $cache) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
Console::info("Skipping project console");
return;
}
Console::log("Getting stats for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$statsPerProject = [];
$statsPerProject['time'] = microtime(true);
/** Get Project ID */
$statsPerProject['project_id'] = $project->getId();
/** Get project created time */
$statsPerProject['project_created'] = $project->getAttribute('$createdAt');
/** Get Project Name */
$statsPerProject['project_name'] = $project->getAttribute('name');
/** Get Total Functions */
$statsPerProject['custom_functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT);
foreach (\array_keys(Config::getParam('runtimes')) as $runtime) {
$statsPerProject['custom_functions_' . $runtime] = $dbForProject->count('functions', [
Query::equal('runtime', [$runtime]),
], APP_LIMIT_COUNT);
}
/** Get Total Deployments */
$statsPerProject['custom_deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT);
/** Get Total Teams */
$statsPerProject['custom_teams'] = $dbForProject->count('teams', [], APP_LIMIT_COUNT);
/** Get Total Members */
$teamInternalId = $project->getAttribute('teamInternalId', null);
if ($teamInternalId) {
$statsPerProject['custom_organization_members'] = $dbForConsole->count('memberships', [
Query::equal('teamInternalId', [$teamInternalId])
], APP_LIMIT_COUNT);
} else {
$statsPerProject['custom_organization_members'] = 0;
}
/** Get Email and Name of the project owner */
if ($teamInternalId) {
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$teamInternalId]),
]);
if (!$membership || $membership->isEmpty()) {
throw new Exception('Membership not found. Skipping project : ' . $project->getId());
}
$userInternalId = $membership->getAttribute('userInternalId', null);
if ($userInternalId) {
$user = $dbForConsole->findOne('users', [
Query::equal('_id', [$userInternalId]),
]);
$statsPerProject['email'] = $user->getAttribute('email', null);
$statsPerProject['name'] = $user->getAttribute('name', null);
}
}
/** Get Domains */
$statsPerProject['custom_domains'] = $dbForConsole->count('domains', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Platforms */
$platforms = $dbForConsole->find('platforms', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
$statsPerProject['custom_platforms_web'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'web';
}));
$statsPerProject['custom_platforms_android'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'android';
}));
$statsPerProject['custom_platforms_apple'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'apple');
}));
$statsPerProject['custom_platforms_flutter'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'flutter');
}));
$flutterPlatforms = [Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_FLUTTER_LINUX];
foreach ($flutterPlatforms as $flutterPlatform) {
$statsPerProject['custom_platforms_' . $flutterPlatform] = sizeof(array_filter($platforms, function ($platform) use ($flutterPlatform) {
return $platform['type'] === $flutterPlatform;
}));
}
$statsPerProject['custom_platforms_api_keys'] = $dbForConsole->count('keys', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Usage $statsPerProject */
$periods = [
'infinity' => [
'period' => '1d',
'limit' => 90,
],
'24h' => [
'period' => '1h',
'limit' => 24,
],
];
Authorization::skip(function () use ($dbForProject, $periods, &$statsPerProject) {
foreach ($this->metrics as $key => $metric) {
foreach ($periods as $periodKey => $periodValue) {
$limit = $periodValue['limit'];
$period = $periodValue['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$statsPerProject[$key . '_' . $periodKey] = [];
foreach ($requestDocs as $requestDoc) {
$statsPerProject[$key . '_' . $periodKey][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$statsPerProject[$key . '_' . $periodKey] = array_reverse($statsPerProject[$key . '_' . $periodKey]);
// Calculate aggregate of each metric
$statsPerProject[$key . '_' . $periodKey] = array_sum(array_column($statsPerProject[$key . '_' . $periodKey], 'value'));
}
}
});
if (isset($statsPerProject['email'])) {
/** Send data to mixpanel */
$res = $this->mixpanel->createProfile($statsPerProject['email'], '', [
'name' => $statsPerProject['name'],
'email' => $statsPerProject['email']
]);
if (!$res) {
Console::error('Failed to create user profile for project: ' . $project->getId());
}
$event = new Event();
$event
->setName('Project Daily Usage')
->setProps($statsPerProject);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
Console::error('Failed to create event for project: ' . $project->getId());
}
}
} catch (Exception $e) {
Console::error('Failed to send stats for project: ' . $project->getId());
Console::error($e->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
});
}
public function action(Group $pools, Cache $cache, Database $dbForConsole): void
{
Console::title('Cloud Hamster V1');
Console::success(APP_NAME . ' cloud hamster process has started');
$sleep = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default)
$jobInitTime = App::getEnv('_APP_HAMSTER_TIME', '22:00'); // (hour:minutes)
$now = new \DateTime();
$now->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$next = new \DateTime($now->format("Y-m-d $jobInitTime"));
$next->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$delay = $next->getTimestamp() - $now->getTimestamp();
/**
* If time passed for the target day.
*/
if ($delay <= 0) {
$next->add(\DateInterval::createFromDateString('1 days'));
$delay = $next->getTimestamp() - $now->getTimestamp();
}
Console::log('[' . $now->format("Y-m-d H:i:s.v") . '] Delaying for ' . $delay . ' setting loop to [' . $next->format("Y-m-d H:i:s.v") . ']');
Console::loop(function () use ($pools, $cache, $dbForConsole, $sleep) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Getting Cloud Usage Stats every {$sleep} seconds");
$loopStart = microtime(true);
/* Initialise new Utopia app */
$app = new App('UTC');
Console::info('Getting stats for all projects');
$this->getStatsPerProject($pools, $cache, $dbForConsole);
Console::success('Completed getting stats for all projects');
Console::info('Getting stats for all organizations');
$this->getStatsPerOrganization($dbForConsole);
Console::success('Completed getting stats for all organizations');
Console::info('Getting stats for all users');
$this->getStatsPerUser($dbForConsole);
Console::success('Completed getting stats for all users');
$pools
->get('console')
->reclaim();
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Cloud Stats took {$loopTook} seconds");
}, $sleep, $delay);
}
protected function calculateByGroup(string $collection, Database $dbForConsole, callable $callback)
{
$count = 0;
$chunk = 0;
$limit = 50;
$results = [];
$sum = $limit;
$executionStart = \microtime(true);
while ($sum === $limit) {
$chunk++;
$results = $dbForConsole->find($collection, \array_merge([
Query::limit($limit),
Query::offset($count)
]));
$sum = count($results);
Console::log('Processing chunk #' . $chunk . '. Found ' . $sum . ' documents');
foreach ($results as $document) {
call_user_func($callback, $dbForConsole, $document);
$count++;
}
}
$executionEnd = \microtime(true);
Console::log("Processed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
}
protected function getStatsPerOrganization(Database $dbForConsole)
{
$this->calculateByGroup('teams', $dbForConsole, function (Database $dbForConsole, Document $document) {
try {
$statsPerOrganization = [];
/** Organization name */
$statsPerOrganization['name'] = $document->getAttribute('name');
/** Get Email and of the organization owner */
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$document->getInternalId()]),
]);
if (!$membership || $membership->isEmpty()) {
throw new Exception('Membership not found. Skipping organization : ' . $document->getId());
}
$userInternalId = $membership->getAttribute('userInternalId', null);
if ($userInternalId) {
$user = $dbForConsole->findOne('users', [
Query::equal('_id', [$userInternalId]),
]);
$statsPerOrganization['email'] = $user->getAttribute('email', null);
}
/** Organization Creation Date */
$statsPerOrganization['created'] = $document->getAttribute('$createdAt');
/** Number of team members */
$statsPerOrganization['members'] = $document->getAttribute('total');
/** Number of projects in this organization */
$statsPerOrganization['projects'] = $dbForConsole->count('projects', [
Query::equal('teamId', [$document->getId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerOrganization['email'])) {
throw new Exception('Email not found. Skipping organization : ' . $document->getId());
}
$event = new Event();
$event
->setName('Organization Daily Usage')
->setProps($statsPerOrganization);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new Exception('Failed to create event for organization : ' . $document->getId());
}
} catch (Exception $e) {
Console::error($e->getMessage());
}
});
}
protected function getStatsPerUser(Database $dbForConsole)
{
$this->calculateByGroup('users', $dbForConsole, function (Database $dbForConsole, Document $document) {
try {
$statsPerUser = [];
/** Organization name */
$statsPerUser['name'] = $document->getAttribute('name');
/** Organization ID (needs to be stored as an email since mixpanel uses the email attribute as a distinctID) */
$statsPerUser['email'] = $document->getAttribute('email');
/** Organization Creation Date */
$statsPerUser['created'] = $document->getAttribute('$createdAt');
/** Number of teams this user is a part of */
$statsPerUser['memberships'] = $dbForConsole->count('memberships', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerUser['email'])) {
throw new Exception('User has no email: ' . $document->getId());
}
/** Send data to mixpanel */
$event = new Event();
$event
->setName('User Daily Usage')
->setProps($statsPerUser);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new Exception('Failed to create user profile for user: ' . $document->getId());
}
} catch (Exception $e) {
Console::error($e->getMessage());
}
});
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Auth\Auth;
use Appwrite\Docker\Compose;
use Appwrite\Docker\Env;
use Appwrite\Utopia\View;
use Utopia\Analytics\Adapter;
use Utopia\Analytics\Adapter\GoogleAnalytics;
use Utopia\Analytics\Event;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Validator\Text;
use Utopia\Platform\Action;
class Install extends Action
{
protected string $path = '/usr/src/code/appwrite';
public static function getName(): string
{
return 'install';
}
public function __construct()
{
$this
->desc('Install Appwrite')
->param('httpPort', '', new Text(4), 'Server HTTP port', true)
->param('httpsPort', '', new Text(4), 'Server HTTPS port', true)
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive));
}
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void
{
$config = Config::getParam('variables');
$defaultHTTPPort = '80';
$defaultHTTPSPort = '443';
$vars = [];
/**
* We are using a random value every execution for identification.
* This allows us to collect information without invading the privacy of our users.
*/
$analytics = new GoogleAnalytics('UA-26264668-9', uniqid('server.', true));
foreach ($config as $category) {
foreach ($category['variables'] ?? [] as $var) {
$vars[] = $var;
}
}
Console::success('Starting Appwrite installation...');
// Create directory with write permissions
if (!\file_exists(\dirname($this->path))) {
if (!@\mkdir(\dirname($this->path), 0755, true)) {
Console::error('Can\'t create directory ' . \dirname($this->path));
Console::exit(1);
}
}
$data = @file_get_contents($this->path . '/docker-compose.yml');
if ($data !== false) {
if ($interactive == 'Y' && Console::isInteractive()) {
$answer = Console::confirm('Previous installation found, do you want to overwrite it (a backup will be created before overwriting)? (Y/n)');
if ($answer !== 'Y') {
Console::info('No action taken.');
return;
}
}
$time = \time();
Console::info('Compose file found, creating backup: docker-compose.yml.' . $time . '.backup');
file_put_contents($this->path . '/docker-compose.yml.' . $time . '.backup', $data);
$compose = new Compose($data);
$appwrite = $compose->getService('appwrite');
$oldVersion = ($appwrite) ? $appwrite->getImageVersion() : null;
try {
$ports = $compose->getService('traefik')->getPorts();
} catch (\Throwable $th) {
$ports = [
$defaultHTTPPort => $defaultHTTPPort,
$defaultHTTPSPort => $defaultHTTPSPort
];
Console::warning('Traefik not found. Falling back to default ports.');
}
if ($oldVersion) {
foreach ($compose->getServices() as $service) { // Fetch all env vars from previous compose file
if (!$service) {
continue;
}
$env = $service->getEnvironment()->list();
foreach ($env as $key => $value) {
if (is_null($value)) {
continue;
}
foreach ($vars as $i => $var) {
if ($var['name'] === $key) {
$vars[$i]['default'] = $value;
}
}
}
}
$data = @file_get_contents($this->path . '/.env');
if ($data !== false) { // Fetch all env vars from previous .env file
Console::info('Env file found, creating backup: .env.' . $time . '.backup');
file_put_contents($this->path . '/.env.' . $time . '.backup', $data);
$env = new Env($data);
foreach ($env->list() as $key => $value) {
if (is_null($value)) {
continue;
}
foreach ($vars as $i => $var) {
if ($var['name'] === $key) {
$vars[$i]['default'] = $value;
}
}
}
}
foreach ($ports as $key => $value) {
if ($value === $defaultHTTPPort) {
$defaultHTTPPort = $key;
}
if ($value === $defaultHTTPSPort) {
$defaultHTTPSPort = $key;
}
}
}
}
if (empty($httpPort)) {
$httpPort = Console::confirm('Choose your server HTTP port: (default: ' . $defaultHTTPPort . ')');
$httpPort = ($httpPort) ? $httpPort : $defaultHTTPPort;
}
if (empty($httpsPort)) {
$httpsPort = Console::confirm('Choose your server HTTPS port: (default: ' . $defaultHTTPSPort . ')');
$httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPSPort;
}
$input = [];
foreach ($vars as $var) {
if (!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) {
if ($data && $var['default'] !== null) {
$input[$var['name']] = $var['default'];
continue;
}
if ($var['filter'] === 'token') {
$input[$var['name']] = Auth::tokenGenerator();
continue;
}
if ($var['filter'] === 'password') {
$input[$var['name']] = Auth::passwordGenerator();
continue;
}
}
if (!$var['required'] || !Console::isInteractive() || $interactive !== 'Y') {
$input[$var['name']] = $var['default'];
continue;
}
$input[$var['name']] = Console::confirm($var['question'] . ' (default: \'' . $var['default'] . '\')');
if (empty($input[$var['name']])) {
$input[$var['name']] = $var['default'];
}
if ($var['filter'] === 'domainTarget') {
if ($input[$var['name']] !== 'localhost') {
Console::warning("\nIf you haven't already done so, set the following record for {$input[$var['name']]} on your DNS provider:\n");
$mask = "%-15.15s %-10.10s %-30.30s\n";
printf($mask, "Type", "Name", "Value");
printf($mask, "A or AAAA", "@", "<YOUR PUBLIC IP>");
Console::warning("\nUse 'AAAA' if you're using an IPv6 address and 'A' if you're using an IPv4 address.\n");
}
}
}
$templateForCompose = new View(__DIR__ . '/../../../../app/views/install/compose.phtml');
$templateForEnv = new View(__DIR__ . '/../../../../app/views/install/env.phtml');
$templateForCompose
->setParam('httpPort', $httpPort)
->setParam('httpsPort', $httpsPort)
->setParam('version', APP_VERSION_STABLE)
->setParam('organization', $organization)
->setParam('image', $image);
$templateForEnv->setParam('vars', $input);
if (!file_put_contents($this->path . '/docker-compose.yml', $templateForCompose->render(false))) {
$message = 'Failed to save Docker Compose file';
$this->sendEvent($analytics, $message);
Console::error($message);
Console::exit(1);
}
if (!file_put_contents($this->path . '/.env', $templateForEnv->render(false))) {
$message = 'Failed to save environment variables file';
$this->sendEvent($analytics, $message);
Console::error($message);
Console::exit(1);
}
$env = '';
$stdout = '';
$stderr = '';
foreach ($input as $key => $value) {
if ($value) {
$env .= $key . '=' . \escapeshellarg($value) . ' ';
}
}
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';
$this->sendEvent($analytics, $message);
Console::error($message);
Console::error($stderr);
Console::exit($exit);
} else {
$message = 'Appwrite installed successfully';
$this->sendEvent($analytics, $message);
Console::success($message);
}
}
private function sendEvent(Adapter $analytics, string $message): void
{
$event = new Event();
$event->setName(APP_VERSION_STABLE);
$event->setValue($message);
$event->setUrl('http://localhost/');
$event->setProps([
'category' => 'install/server',
'action' => 'install',
]);
$event->setType('install/server');
$analytics->createEvent($event);
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Query;
use Utopia\Platform\Action;
class Maintenance extends Action
{
public static function getName(): string
{
return 'maintenance';
}
public function __construct()
{
$this
->desc('Schedules maintenance tasks and publishes them to resque')
->inject('dbForConsole')
->callback(fn (Database $dbForConsole) => $this->action($dbForConsole));
}
public function action(Database $dbForConsole): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
function notifyDeleteExecutionLogs(int $interval)
{
(new Delete())
->setType(DELETE_TYPE_EXECUTIONS)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
function notifyDeleteAbuseLogs(int $interval)
{
(new Delete())
->setType(DELETE_TYPE_ABUSE)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
function notifyDeleteAuditLogs(int $interval)
{
(new Delete())
->setType(DELETE_TYPE_AUDIT)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
function notifyDeleteUsageStats(int $usageStatsRetentionHourly)
{
(new Delete())
->setType(DELETE_TYPE_USAGE)
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->trigger();
}
function notifyDeleteConnections()
{
(new Delete())
->setType(DELETE_TYPE_REALTIME)
->setDatetime(DateTime::addSeconds(new \DateTime(), -60))
->trigger();
}
function notifyDeleteExpiredSessions()
{
(new Delete())
->setType(DELETE_TYPE_SESSIONS)
->trigger();
}
function renewCertificates($dbForConsole)
{
$time = DateTime::now();
$certificates = $dbForConsole->find('certificates', [
Query::lessThan('attempts', 5), // Maximum 5 attempts
Query::lessThanEqual('renewDate', $time), // includes 60 days cooldown (we have 30 days to renew)
Query::limit(200), // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
]);
if (\count($certificates) > 0) {
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
$event = new Certificate();
foreach ($certificates as $certificate) {
$event
->setDomain(new Document([
'domain' => $certificate->getAttribute('domain')
]))
->trigger();
}
} else {
Console::info("[{$time}] No certificates for renewal.");
}
}
function notifyDeleteCache($interval)
{
(new Delete())
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
function notifyDeleteSchedules($interval)
{
(new Delete())
->setType(DELETE_TYPE_SCHEDULES)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
// # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
$auditLogRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', '1209600');
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
$usageStatsRetentionHourly = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_HOURLY', '8640000'); //100 days
$cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
$schedulesDeletionRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForConsole) {
$time = DateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetentionHourly);
notifyDeleteConnections();
notifyDeleteExpiredSessions();
renewCertificates($dbForConsole);
notifyDeleteCache($cacheRetention);
notifyDeleteSchedules($schedulesDeletionRetention);
}, $interval);
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Appwrite\Migration\Migration;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Text;
class Migrate extends Action
{
public static function getName(): string
{
return 'migrate';
}
public function __construct()
{
$this
->desc('Migrate Appwrite to new version')
/** @TODO APP_VERSION_STABLE needs to be defined */
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
->inject('cache')
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn ($version, $cache, $dbForConsole, $getProjectDB) => $this->action($version, $cache, $dbForConsole, $getProjectDB));
}
private function clearProjectsCache(Cache $cache, Document $project)
{
try {
$cache->purge("cache-_{$project->getInternalId()}:*");
} catch (\Throwable $th) {
Console::error('Failed to clear project ("' . $project->getId() . '") cache with error: ' . $th->getMessage());
}
}
public function action(string $version, Cache $cache, Database $dbForConsole, callable $getProjectDB)
{
Authorization::disable();
if (!array_key_exists($version, Migration::$versions)) {
Console::error("Version {$version} not found.");
Console::exit(1);
return;
}
$app = new App('UTC');
Console::success('Starting Data Migration to version ' . $version);
$console = $app->getResource('console');
$limit = 30;
$sum = 30;
$offset = 0;
/**
* @var \Utopia\Database\Document[] $projects
*/
$projects = [$console];
$count = 0;
try {
$totalProjects = $dbForConsole->count('projects') + 1;
} catch (\Throwable $th) {
$dbForConsole->setNamespace('_console');
$totalProjects = $dbForConsole->count('projects') + 1;
}
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
/** @var Migration $migration */
$migration = new $class();
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console' && $project->getInternalId() !== 'console') {
continue;
}
$this->clearProjectsCache($cache, $project);
try {
// TODO: Iterate through all project DBs
$projectDB = $getProjectDB($project);
$migration
->setProject($project, $projectDB, $dbForConsole)
->execute();
} catch (\Throwable $th) {
Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage());
throw $th;
}
$this->clearProjectsCache($cache, $project);
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($offset)]);
$offset = $offset + $limit;
$count = $count + $sum;
Console::log('Migrated ' . $count . '/' . $totalProjects . ' projects...');
}
Console::success('Data Migration Completed');
}
}
@@ -0,0 +1,97 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
class PatchCreateMissingSchedules extends Action
{
public static function getName(): string
{
return 'patch-create-missing-schedules';
}
public function __construct()
{
$this
->desc('Ensure every function has a schedule')
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn (Database $dbForConsole, callable $getProjectDB) => $this->action($dbForConsole, $getProjectDB));
}
/**
* Iterate over every function on every project to make sure there is a schedule. If not, recreate the schedule.
*/
public function action(Database $dbForConsole, callable $getProjectDB): void
{
Authorization::disable();
Authorization::setDefaultStatus(false);
Console::title('PatchCreateMissingSchedules V1');
Console::success(APP_NAME . ' PatchCreateMissingSchedules v1 has started');
$limit = 100;
$projectCursor = null;
while (true) {
$projectsQueries = [Query::limit($limit)];
if ($projectCursor !== null) {
$projectsQueries[] = Query::cursorAfter($projectCursor);
}
$projects = $dbForConsole->find('projects', $projectsQueries);
if (count($projects) === 0) {
break;
}
foreach ($projects as $project) {
Console::log("Checking Project " . $project->getAttribute('name') . " (" . $project->getId() . ")");
$dbForProject = $getProjectDB($project);
$functionCursor = null;
while (true) {
$functionsQueries = [Query::limit($limit)];
if ($functionCursor !== null) {
$functionsQueries[] = Query::cursorAfter($functionCursor);
}
$functions = $dbForProject->find('functions', $functionsQueries);
if (count($functions) === 0) {
break;
}
foreach ($functions as $function) {
$scheduleId = $function->getAttribute('scheduleId');
$schedule = $dbForConsole->getDocument('schedules', $scheduleId);
if ($schedule->isEmpty()) {
$functionId = $function->getId();
$schedule = $dbForConsole->createDocument('schedules', new Document([
'$id' => ID::custom($scheduleId),
'region' => $project->getAttribute('region', 'default'),
'resourceType' => 'function',
'resourceId' => $functionId,
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $function->getAttribute('schedule'),
'active' => !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')),
]));
Console::success('Recreated schedule for function ' . $functionId);
}
}
$functionCursor = $functions[array_key_last($functions)];
}
}
$projectCursor = $projects[array_key_last($projects)];
}
}
}
@@ -0,0 +1,129 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Pools\Group;
use Utopia\Validator\Numeric;
class PatchDeleteProjectCollections extends Action
{
private array $names = [
'webhooks',
'platforms',
'schedules',
'projects',
'domains',
'certificates',
'keys',
'realtime',
];
public static function getName(): string
{
return 'patch-delete-project-collections';
}
public function __construct()
{
$this
->desc('Delete unnecessary project collections')
->param('offset', 0, new Numeric(), 'Resume deletion from param pos', true)
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->callback(function (int $offset, Group $pools, Cache $cache, Database $dbForConsole) {
$this->action($offset, $pools, $cache, $dbForConsole);
});
}
public function action(int $offset, Group $pools, Cache $cache, Database $dbForConsole): void
{
//docker compose exec -t appwrite patch-delete-project-collections
Console::title('Delete project collections V1');
Console::success(APP_NAME . ' delete project collections has started');
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
/** Database connections */
$totalProjects = $dbForConsole->count('projects');
Console::success("Found a total of: {$totalProjects} projects");
$projects = [$console];
$count = 0;
$limit = 50;
$sum = 50;
$offset = $offset;
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
continue;
}
Console::info("Deleting collections for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForProject->setNamespace('_' . $project->getInternalId());
foreach ($this->names as $name) {
if (empty($name)) {
continue;
}
if ($dbForProject->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'), $name)) {
if ($dbForProject->deleteCollection($name)) {
Console::log('Deleted ' . $name);
} else {
Console::error('Failed to delete ' . $name);
}
}
}
} catch (\Throwable $th) {
Console::error('Failed on project ("' . $project->getId() . '") version with error: ' . $th->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [
Query::limit($limit),
Query::offset($offset),
]);
if (!empty($projects)) {
Console::log('Querying..... offset=' . $offset . ' , limit=' . $limit . ', count=' . $count);
}
$offset = $offset + $limit;
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects...');
$pools
->get('console')
->reclaim();
}
}
@@ -0,0 +1,74 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\Query;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Pools\Group;
class PatchDeleteScheduleUpdatedAtAttribute extends Action
{
public static function getName(): string
{
return 'patch-delete-schedule-updated-at-attribute';
}
public function __construct()
{
$this
->desc('Ensure function collections do not have scheduleUpdatedAt attribute')
->inject('pools')
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB));
}
/**
* Iterate over every function on every project to make sure there is a schedule. If not, recreate the schedule.
*/
public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void
{
Authorization::disable();
Authorization::setDefaultStatus(false);
Console::title('PatchDeleteScheduleUpdatedAtAttribute V1');
Console::success(APP_NAME . ' PatchDeleteScheduleUpdatedAtAttribute v1 has started');
$limit = 100;
$projectCursor = null;
while (true) {
$projectsQueries = [Query::limit($limit)];
if ($projectCursor !== null) {
$projectsQueries[] = Query::cursorAfter($projectCursor);
}
$projects = $dbForConsole->find('projects', $projectsQueries);
if (count($projects) === 0) {
break;
}
foreach ($projects as $project) {
Console::log("Checking Project " . $project->getAttribute('name') . " (" . $project->getId() . ")");
$dbForProject = $getProjectDB($project);
try {
/**
* Delete 'scheduleUpdatedAt' attribute
*/
$dbForProject->deleteAttribute('functions', 'scheduleUpdatedAt');
$dbForProject->deleteCachedCollection('functions');
Console::success("'scheduleUpdatedAt' deleted.");
} catch (\Throwable $th) {
Console::warning("'scheduleUpdatedAt' errored: {$th->getMessage()}");
}
$pools->reclaim();
}
$projectCursor = $projects[array_key_last($projects)];
}
}
}
+294
View File
@@ -0,0 +1,294 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Appwrite\SDK\Language\Android;
use Appwrite\SDK\Language\CLI;
use Appwrite\SDK\Language\Dart;
use Appwrite\SDK\Language\Deno;
use Appwrite\SDK\Language\DotNet;
use Appwrite\SDK\Language\Flutter;
use Appwrite\SDK\Language\Go;
use Appwrite\SDK\Language\GraphQL;
use Appwrite\SDK\Language\Kotlin;
use Appwrite\SDK\Language\Node;
use Appwrite\SDK\Language\PHP;
use Appwrite\SDK\Language\Python;
use Appwrite\SDK\Language\REST;
use Appwrite\SDK\Language\Ruby;
use Appwrite\SDK\Language\Swift;
use Exception;
use Throwable;
use Appwrite\SDK\Language\Apple;
use Appwrite\SDK\Language\Web;
use Appwrite\SDK\SDK;
use Appwrite\Spec\Swagger2;
use Utopia\CLI\Console;
use Utopia\Config\Config;
class SDKs extends Action
{
public static function getName(): string
{
return 'sdks';
}
public function __construct()
{
$this
->desc('Generate Appwrite SDKs')
->callback(fn () => $this->action());
}
public function action(): void
{
$platforms = Config::getParam('platforms');
$selectedPlatform = Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version = Console::confirm('Choose an Appwrite version');
$git = (Console::confirm('Should we use git push? (yes/no)') == 'yes');
$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', 'latest'])) {
throw new Exception('Unknown version given');
}
foreach ($platforms as $key => $platform) {
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
continue;
}
foreach ($platform['sdks'] as $language) {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*') {
continue;
}
if (!$language['enabled']) {
Console::warning($language['name'] . ' for ' . $platform['name'] . ' is disabled');
continue;
}
Console::info('Fetching API Spec for ' . $language['name'] . ' for ' . $platform['name'] . ' (version: ' . $version . ')');
$spec = file_get_contents(__DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json');
$cover = 'https://appwrite.io/images/github.png';
$result = \realpath(__DIR__ . '/../../../../app') . '/sdks/' . $key . '-' . $language['key'];
$resultExamples = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version . '/' . $key . '-' . $language['key'];
$target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/' . $language['key'] . '/';
$readme = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/README.md');
$readme = ($readme) ? \file_get_contents($readme) : '';
$gettingStarted = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/GETTING_STARTED.md');
$gettingStarted = ($gettingStarted) ? \file_get_contents($gettingStarted) : '';
$examples = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/EXAMPLES.md');
$examples = ($examples) ? \file_get_contents($examples) : '';
$changelog = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/CHANGELOG.md');
$changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log';
$warning = '**This SDK is compatible with Appwrite server version ' . $version . '. For older versions, please check [previous releases](' . $language['url'] . '/releases).**';
$license = 'BSD-3-Clause';
$licenseContent = 'Copyright (c) ' . date('Y') . ' Appwrite (https://appwrite.io) and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.';
switch ($language['key']) {
case 'web':
$config = new Web();
if ($platform['key'] === APP_PLATFORM_CONSOLE) {
$config->setNPMPackage('@appwrite.io/console');
$config->setBowerPackage('@appwrite.io/console');
} else {
$config->setNPMPackage('appwrite');
$config->setBowerPackage('appwrite');
}
break;
case 'cli':
$config = new CLI();
$config->setNPMPackage('appwrite-cli');
$config->setExecutableName('appwrite');
$config->setLogo(json_encode("
_ _ _ ___ __ _____
/_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \
//_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/
/ _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_
\_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/
|_| |_|
"));
$config->setLogoUnescaped("
_ _ _ ___ __ _____
/_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \
//_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/
/ _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_
\_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/
|_| |_| ");
break;
case 'php':
$config = new PHP();
$config->setComposerVendor('appwrite');
$config->setComposerPackage('appwrite');
break;
case 'nodejs':
$config = new Node();
$config->setNPMPackage('node-appwrite');
$config->setBowerPackage('appwrite');
$warning = $warning . "\n\n > This is the Node.js SDK for integrating with Appwrite from your Node.js server-side code.
If you're looking to integrate from the browser, you should check [appwrite/sdk-for-web](https://github.com/appwrite/sdk-for-web)";
break;
case 'deno':
$config = new Deno();
break;
case 'python':
$config = new Python();
$config->setPipPackage('appwrite');
$license = 'BSD License'; // license edited due to classifiers in pypi
break;
case 'ruby':
$config = new Ruby();
$config->setGemPackage('appwrite');
break;
case 'flutter':
$config = new Flutter();
$config->setPackageName('appwrite');
break;
case 'flutter-dev':
$config = new Flutter();
$config->setPackageName('appwrite_dev');
break;
case 'dart':
$config = new Dart();
$config->setPackageName('dart_appwrite');
$warning = $warning . "\n\n > This is the Dart SDK for integrating with Appwrite from your Dart server-side code. If you're looking for the Flutter SDK you should check [appwrite/sdk-for-flutter](https://github.com/appwrite/sdk-for-flutter)";
break;
case 'go':
$config = new Go();
break;
case 'swift':
$config = new Swift();
$warning = $warning . "\n\n > This is the Swift SDK for integrating with Appwrite from your Swift server-side code. If you're looking for the Apple SDK you should check [appwrite/sdk-for-apple](https://github.com/appwrite/sdk-for-apple)";
break;
case 'apple':
$config = new Apple();
break;
case 'dotnet':
$cover = '';
$config = new DotNet();
break;
case 'android':
$config = new Android();
break;
case 'kotlin':
$config = new Kotlin();
$warning = $warning . "\n\n > This is the Kotlin SDK for integrating with Appwrite from your Kotlin server-side code. If you're looking for the Android SDK you should check [appwrite/sdk-for-android](https://github.com/appwrite/sdk-for-android)";
break;
case 'graphql':
$config = new GraphQL();
break;
case 'rest':
$config = new REST();
break;
default:
throw new Exception('Language "' . $language['key'] . '" not supported');
}
Console::info("Generating {$language['name']} SDK...");
$sdk = new SDK($config, new Swagger2($spec));
$sdk
->setName($language['name'])
->setNamespace('io appwrite')
->setDescription("Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the {$language['name']} SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)")
->setShortDescription('Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API')
->setLicense($license)
->setLicenseContent($licenseContent)
->setVersion($language['version'])
->setPlatform($key)
->setGitURL($language['url'])
->setGitRepo($language['gitUrl'])
->setGitRepoName($language['gitRepoName'])
->setGitUserName($language['gitUserName'])
->setLogo($cover)
->setURL('https://appwrite.io')
->setShareText('Appwrite is a backend as a service for building web or mobile apps')
->setShareURL('http://appwrite.io')
->setShareTags('JS,javascript,reactjs,angular,ios,android,serverless')
->setShareVia('appwrite')
->setWarning($warning)
->setReadme($readme)
->setGettingStarted($gettingStarted)
->setChangelog($changelog)
->setExamples($examples)
->setTwitter(APP_SOCIAL_TWITTER_HANDLE)
->setDiscord(APP_SOCIAL_DISCORD_CHANNEL, APP_SOCIAL_DISCORD)
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '1.4.0',
]);
try {
$sdk->generate($result);
} catch (Exception $exception) {
Console::error($exception->getMessage());
} catch (Throwable $exception) {
Console::error($exception->getMessage());
}
$gitUrl = $language['gitUrl'];
$gitBranch = $language['gitBranch'];
if (!$production) {
$gitUrl = 'git@github.com:aw-tests/' . $language['gitRepoName'] . '.git';
}
if ($git && !empty($gitUrl)) {
\exec('rm -rf ' . $target . ' && \
mkdir -p ' . $target . ' && \
cd ' . $target . ' && \
git init --initial-branch=' . $gitBranch . ' && \
git remote add origin ' . $gitUrl . ' && \
git fetch origin ' . $gitBranch . ' && \
git pull origin ' . $gitBranch . ' && \
rm -rf ' . $target . '/* && \
cp -r ' . $result . '/* ' . $target . '/ && \
git add . && \
git commit -m "' . $message . '" && \
git push -u origin ' . $gitBranch . '
');
Console::success("Pushed {$language['name']} SDK to {$gitUrl}");
\exec('rm -rf ' . $target);
Console::success("Remove temp directory '{$target}' for {$language['name']} SDK");
}
$docDirectories = $language['docDirectories'] ?? [''];
if ($version === 'latest') {
continue;
}
foreach ($docDirectories as $languageTitle => $path) {
$languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : '');
\exec(
'mkdir -p ' . $resultExamples . $languagePath . ' && \
cp -r ' . $result . '/docs/examples' . $languagePath . ' ' . $resultExamples
);
Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}");
}
}
}
Console::exit();
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Appwrite\Event\Certificate;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Validator\Hostname;
class SSL extends Action
{
public static function getName(): string
{
return 'ssl';
}
public function __construct()
{
$this
->desc('Validate server certificates')
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->callback(fn ($domain) => $this->action($domain));
}
public function action(string $domain): void
{
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
(new Certificate())
->setDomain(new Document([
'domain' => $domain
]))
->setSkipRenewCheck(true)
->trigger();
}
}
+237
View File
@@ -0,0 +1,237 @@
<?php
namespace Appwrite\Platform\Tasks;
use Cron\CronExpression;
use Swoole\Timer;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Database;
use Utopia\Pools\Group;
use Appwrite\Event\Func;
use function Swoole\Coroutine\run;
class Schedule extends Action
{
public const FUNCTION_UPDATE_TIMER = 10; //seconds
public const FUNCTION_ENQUEUE_TIMER = 60; //seconds
public static function getName(): string
{
return 'schedule';
}
public function __construct()
{
$this
->desc('Execute functions scheduled in Appwrite')
->inject('pools')
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB));
}
/**
* 1. Load all documents from 'schedules' collection to create local copy
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutime sleeps until exact time before sending request to worker.
*/
public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void
{
Console::title('Scheduler V1');
Console::success(APP_NAME . ' Scheduler v1 has started');
/**
* Extract only nessessary attributes to lower memory used.
*
* @var Document $schedule
* @return array
*/
$getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array {
$project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId'));
$function = $getProjectDB($project)->getDocument('functions', $schedule->getAttribute('resourceId'));
return [
'resourceId' => $schedule->getAttribute('resourceId'),
'schedule' => $schedule->getAttribute('schedule'),
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'project' => $project, // TODO: @Meldiron Send only ID to worker to reduce memory usage here
'function' => $function, // TODO: @Meldiron Send only ID to worker to reduce memory usage here
];
};
$schedules = []; // Local copy of 'schedules' collection
$lastSyncUpdate = DateTime::now();
$limit = 10000;
$sum = $limit;
$total = 0;
$loadStart = \microtime(true);
$latestDocument = null;
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument !== null) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [App::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', ['function']),
Query::equal('active', [true]),
]));
$sum = count($results);
$total = $total + $sum;
foreach ($results as $document) {
$schedules[$document['resourceId']] = $getSchedule($document);
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
}
$pools->reclaim();
Console::success("{$total} functions were loaded in " . (microtime(true) - $loadStart) . " seconds");
Console::success("Starting timers at " . DateTime::now());
run(
function () use ($dbForConsole, &$schedules, &$lastSyncUpdate, $getSchedule, $pools) {
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(self::FUNCTION_UPDATE_TIMER * 1000, function () use ($dbForConsole, &$schedules, &$lastSyncUpdate, $getSchedule, $pools) {
$time = DateTime::now();
$timerStart = \microtime(true);
$limit = 1000;
$sum = $limit;
$total = 0;
$latestDocument = null;
Console::log("Sync tick: Running at $time");
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument !== null) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [App::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', ['function']),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));
$sum = count($results);
$total = $total + $sum;
foreach ($results as $document) {
$localDocument = $schedules[$document['resourceId']] ?? null;
$org = $localDocument !== null ? strtotime($localDocument['resourceUpdatedAt']) : null;
$new = strtotime($document['resourceUpdatedAt']);
if ($document['active'] === false) {
Console::info("Removing: {$document['resourceId']}");
unset($schedules[$document['resourceId']]);
} elseif ($new !== $org) {
Console::info("Updating: {$document['resourceId']}");
$schedules[$document['resourceId']] = $getSchedule($document);
}
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
}
$lastSyncUpdate = $time;
$timerEnd = \microtime(true);
$pools->reclaim();
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
});
/**
* The timer to prepare soon-to-execute schedules.
*/
$lastEnqueueUpdate = null;
$enqueueFunctions = function () use (&$schedules, $lastEnqueueUpdate, $pools) {
$timerStart = \microtime(true);
$time = DateTime::now();
$enqueueDiff = $lastEnqueueUpdate === null ? 0 : $timerStart - $lastEnqueueUpdate;
$timeFrame = DateTime::addSeconds(new \DateTime(), self::FUNCTION_ENQUEUE_TIMER - $enqueueDiff);
Console::log("Enqueue tick: started at: $time (with diff $enqueueDiff)");
$total = 0;
$delayedExecutions = []; // Group executions with same delay to share one coroutine
foreach ($schedules as $key => $schedule) {
$cron = new CronExpression($schedule['schedule']);
$nextDate = $cron->getNextRunDate();
$next = DateTime::format($nextDate);
$currentTick = $next < $timeFrame;
if (!$currentTick) {
continue;
}
$total++;
$promiseStart = \time(); // in seconds
$executionStart = $nextDate->getTimestamp(); // in seconds
$delay = $executionStart - $promiseStart; // Time to wait from now until execution needs to be queued
if (!isset($delayedExecutions[$delay])) {
$delayedExecutions[$delay] = [];
}
$delayedExecutions[$delay][] = $key;
}
foreach ($delayedExecutions as $delay => $scheduleKeys) {
\go(function () use ($delay, $schedules, $scheduleKeys, $pools) {
\sleep($delay); // in seconds
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
foreach ($scheduleKeys as $scheduleKey) {
// Ensure schedule was not deleted
if (!isset($schedules[$scheduleKey])) {
return;
}
$schedule = $schedules[$scheduleKey];
$functions = new Func($connection);
$functions
->setType('schedule')
->setFunction($schedule['function'])
->setProject($schedule['project'])
->trigger();
}
$queue->reclaim();
});
}
$timerEnd = \microtime(true);
$lastEnqueueUpdate = $timerStart;
Console::log("Enqueue tick: {$total} executions were enqueued in " . ($timerEnd - $timerStart) . " seconds");
};
Timer::tick(self::FUNCTION_ENQUEUE_TIMER * 1000, fn() => $enqueueFunctions());
$enqueueFunctions();
}
);
}
}
+277
View File
@@ -0,0 +1,277 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Utopia\Validator\Text;
use Appwrite\Specification\Format\OpenAPI3;
use Appwrite\Specification\Format\Swagger2;
use Appwrite\Specification\Specification;
use Appwrite\Utopia\Response;
use Exception;
use Swoole\Http\Response as HttpResponse;
use Utopia\App;
use Utopia\Cache\Adapter\None;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Registry\Registry;
use Utopia\Request;
use Utopia\Validator\WhiteList;
class Specs extends Action
{
public static function getName(): string
{
return 'specs';
}
public function __construct()
{
$this
->desc('Generate Appwrite API specifications')
->param('version', 'latest', new Text(16), 'Spec version', true)
->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true)
->inject('register')
->callback(fn (string $version, string $mode, Registry $register) => $this->action($version, $mode, $register));
}
public function action(string $version, string $mode, Registry $register): void
{
$appRoutes = App::getRoutes();
$response = new Response(new HttpResponse());
$mocks = ($mode === 'mocks');
// Mock dependencies
App::setResource('request', fn () => new Request());
App::setResource('response', fn () => $response);
App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$platforms = [
'client' => APP_PLATFORM_CLIENT,
'server' => APP_PLATFORM_SERVER,
'console' => APP_PLATFORM_CONSOLE,
];
$authCounts = [
'client' => 1,
'server' => 2,
'console' => 1,
];
$keys = [
APP_PLATFORM_CLIENT => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_SERVER => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_CONSOLE => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
'Mode' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Mode',
'description' => '',
'in' => 'header',
],
],
];
foreach ($platforms as $platform) {
$routes = [];
$models = [];
$services = [];
foreach ($appRoutes as $key => $method) {
foreach ($method as $route) {
/** @var \Utopia\Route $route */
$routeSecurity = $route->getLabel('sdk.auth', []);
$sdkPlaforms = [];
foreach ($routeSecurity as $value) {
switch ($value) {
case APP_AUTH_TYPE_SESSION:
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
break;
case APP_AUTH_TYPE_KEY:
$sdkPlaforms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_JWT:
$sdkPlaforms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_ADMIN:
$sdkPlaforms[] = APP_PLATFORM_CONSOLE;
break;
}
}
if (empty($routeSecurity)) {
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
}
if (!$route->getLabel('docs', true)) {
continue;
}
if ($route->getLabel('sdk.mock', false) && !$mocks) {
continue;
}
if (!$route->getLabel('sdk.mock', false) && $mocks) {
continue;
}
if (empty($route->getLabel('sdk.namespace', null))) {
continue;
}
if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlaforms)) {
continue;
}
$routes[] = $route;
}
}
foreach (Config::getParam('services', []) as $service) {
if (
!isset($service['docs']) // Skip service if not part of the public API
|| !isset($service['sdk'])
|| !$service['docs']
|| !$service['sdk']
) {
continue;
}
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
'x-globalAttributes' => $service['globalAttributes'] ?? [],
];
}
$models = $response->getModels();
foreach ($models as $key => $value) {
if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
}
}
$arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0];
foreach (['swagger2', 'open-api3'] as $format) {
$formatInstance = match ($format) {
'swagger2' => new Swagger2(...$arguments),
'open-api3' => new OpenAPI3(...$arguments),
default => throw new Exception('Format not found: ' . $format)
};
$specs = new Specification($formatInstance);
$endpoint = App::getEnv('_APP_HOME', '[HOSTNAME]');
$email = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$formatInstance
->setParam('name', APP_NAME)
->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)')
->setParam('endpoint', 'https://HOSTNAME/v1')
->setParam('version', APP_VERSION_STABLE)
->setParam('terms', $endpoint . '/policy/terms')
->setParam('support.email', $email)
->setParam('support.url', $endpoint . '/support')
->setParam('contact.name', APP_NAME . ' Team')
->setParam('contact.email', $email)
->setParam('contact.url', $endpoint . '/support')
->setParam('license.name', 'BSD-3-Clause')
->setParam('license.url', 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE')
->setParam('docs.description', 'Full API docs, specs and tutorials')
->setParam('docs.url', $endpoint . '/docs');
if ($mocks) {
$path = __DIR__ . '/../config/specs/' . $format . '-mocks-' . $platform . '.json';
if (!file_put_contents($path, json_encode($specs->parse()))) {
throw new Exception('Failed to save mocks spec file: ' . $path);
}
Console::success('Saved mocks spec file: ' . realpath($path));
continue;
}
$path = __DIR__ . '/../../../../app/config/specs/' . $format . '-' . $version . '-' . $platform . '.json';
if (!file_put_contents($path, json_encode($specs->parse()))) {
throw new Exception('Failed to save spec file: ' . $path);
}
Console::success('Saved spec file: ' . realpath($path));
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Validator\Text;
class Upgrade extends Install
{
public static function getName(): string
{
return 'upgrade';
}
public function __construct()
{
$this
->desc('Upgrade Appwrite')
->param('httpPort', '', new Text(4), 'Server HTTP port', true)
->param('httpsPort', '', new Text(4), 'Server HTTPS port', true)
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive));
}
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void
{
// Check for previous installation
$data = @file_get_contents($this->path . '/docker-compose.yml');
if (empty($data)) {
Console::error('Appwrite installation not found.');
Console::log('The command was not run in the parent folder of your appwrite installation.');
Console::log('Please navigate to the parent directory of the Appwrite installation and try again.');
Console::log(' parent_directory <= you run the command in this directory');
Console::log(' └── appwrite');
Console::log(' └── docker-compose.yml');
Console::exit(1);
}
parent::action($httpPort, $httpsPort, $organization, $image, $interactive);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Usage\Calculators\TimeSeries;
use InfluxDB\Database as InfluxDatabase;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database as UtopiaDatabase;
use Throwable;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
class Usage extends Action
{
public static function getName(): string
{
return 'usage';
}
public function __construct()
{
$this
->desc('Schedules syncing data from influxdb to Appwrite console db')
->inject('dbForConsole')
->inject('influxdb')
->inject('register')
->inject('getProjectDB')
->inject('logError')
->callback(fn ($dbForConsole, $influxDB, $register, $getProjectDB, $logError) => $this->action($dbForConsole, $influxDB, $register, $getProjectDB, $logError));
}
protected function aggregateTimeseries(UtopiaDatabase $database, InfluxDatabase $influxDB, callable $logError): void
{
}
public function action(UtopiaDatabase $dbForConsole, InfluxDatabase $influxDB, Registry $register, callable $getProjectDB, callable $logError)
{
Console::title('Usage Aggregation V1');
Console::success(APP_NAME . ' usage aggregation process v1 has started');
$errorLogger = fn(Throwable $error, string $action = 'syncUsageStats') => $logError($error, "usage", $action);
$interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '30'); // 30 seconds (by default)
$region = App::getEnv('region', 'default');
$usage = new TimeSeries($region, $dbForConsole, $influxDB, $getProjectDB, $register, $errorLogger);
Console::loop(function () use ($interval, $usage) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating Timeseries Usage data every {$interval} seconds");
$loopStart = microtime(true);
$usage->collect();
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
class Vars extends Action
{
public static function getName(): string
{
return 'vars';
}
public function __construct()
{
$this
->desc('List all the server environment variables')
->callback(fn () => $this->action());
}
public function action(): void
{
$config = Config::getParam('variables', []);
$vars = [];
foreach ($config as $category) {
foreach ($category['variables'] ?? [] as $var) {
$vars[] = $var;
}
}
foreach ($vars as $key => $value) {
Console::log('- ' . $value['name'] . '=' . App::getEnv($value['name'], ''));
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
class Version extends Action
{
public static function getName(): string
{
return 'version';
}
public function __construct()
{
$this
->desc('Get the server version')
->callback(function () {
Console::log(App::getEnv('_APP_VERSION', 'UNKNOWN'));
});
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Platform\Action;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
class VolumeSync extends Action
{
public static function getName(): string
{
return 'volume-sync';
}
public function __construct()
{
$this
->desc('Runs rsync to sync certificates between the storage mount and traefik.')
->param('source', null, new Text(255), 'Source path to sync from.', false)
->param('destination', null, new Text(255), 'Destination path to sync to.', false)
->param('interval', null, new Integer(true), 'Interval to run rsync', false)
->callback(fn ($source, $destination, $interval) => $this->action($source, $destination, $interval));
}
public function action(string $source, string $destination, int $interval)
{
Console::title('RSync V1');
Console::success(APP_NAME . ' rsync process v1 has started');
if (!file_exists($source)) {
Console::error('Source directory does not exist. Exiting ... ');
Console::exit(0);
}
Console::loop(function () use ($interval, $source, $destination) {
$time = DateTime::now();
Console::info("[{$time}] Executing rsync every {$interval} seconds");
Console::info("Syncing between $source and $destination");
if (!file_exists($source)) {
Console::error('Source directory does not exist. Skipping ... ');
return;
}
$stdin = "";
$stdout = "";
$stderr = "";
Console::execute("rsync -av $source $destination", $stdin, $stdout, $stderr);
Console::success($stdout);
Console::error($stderr);
}, $interval);
}
}
+177 -120
View File
@@ -2,21 +2,24 @@
namespace Appwrite\Resque;
use Appwrite\Event\Usage;
use Exception;
use Utopia\App;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Cache\Adapter\Sharding;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Pools\Group;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Backblaze;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Device\Linode;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\S3;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Storage\Device\Wasabi;
use Utopia\Storage\Storage;
@@ -137,7 +140,12 @@ abstract class Worker
*/
public function tearDown(): void
{
global $register;
try {
$pools = $register->get('pools'); /** @var Group $pools */
$pools->reclaim();
$this->shutdown();
} catch (\Throwable $error) {
foreach (self::$errorCallbacks as $errorCallback) {
@@ -161,24 +169,43 @@ abstract class Worker
/**
* Get internal project database
* @param string $projectId
* @param Document $project
* @return Database
* @throws Exception
*/
protected function getProjectDB(string $projectId, ?Document $project = null): Database
protected static $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
protected function getProjectDB(Document $project): Database
{
if ($project === null) {
$consoleDB = $this->getConsoleDB();
global $register;
if ($projectId === 'console') {
return $consoleDB;
}
$pools = $register->get('pools'); /** @var Group $pools */
/** @var Document $project */
$project = Authorization::skip(fn() => $consoleDB->getDocument('projects', $projectId));
if ($project->isEmpty() || $project->getId() === 'console') {
return $this->getConsoleDB();
}
return $this->getDB(self::DATABASE_PROJECT, $projectId, $project->getInternalId(), $project);
$databaseName = $project->getAttribute('database');
if (isset(self::$databases[$databaseName])) {
$database = self::$databases[$databaseName];
$database->setNamespace('_' . $project->getInternalId());
return $database;
}
$dbAdapter = $pools
->get($project->getAttribute('database'))
->pop()
->getResource()
;
$database = new Database($dbAdapter, $this->getCache());
self::$databases[$databaseName] = $database;
$database->setNamespace('_' . $project->getInternalId());
return $database;
}
/**
@@ -188,81 +215,74 @@ abstract class Worker
*/
protected function getConsoleDB(): Database
{
return $this->getDB(self::DATABASE_CONSOLE);
global $register;
$pools = $register->get('pools'); /** @var Group $pools */
$databaseName = 'console';
if (isset(self::$databases[$databaseName])) {
$database = self::$databases[$databaseName];
$database->setNamespace('_console');
return $database;
}
$dbAdapter = $pools
->get('console')
->pop()
->getResource()
;
$database = new Database($dbAdapter, $this->getCache());
self::$databases[$databaseName] = $database;
$database->setNamespace('_console');
return $database;
}
/**
* Get Cache
* @return Cache
*/
protected function getCache(): Cache
{
global $register;
$pools = $register->get('pools'); /** @var Group $pools */
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = $pools
->get($value)
->pop()
->getResource()
;
}
return new Cache(new Sharding($adapters));
}
/**
* Get database
* @param string $type One of (project, console)
* @param string $projectId of project or console DB
* @param string $projectInternalId
* @param Document|null $project
* @return Database
* Get usage queue
* @return Usage
* @throws Exception
*/
private function getDB(
string $type,
string $projectId = '',
string $projectInternalId = '',
?Document $project = null
): Database {
protected function getUsageQueue(): Usage
{
global $register;
$sleep = DATABASE_RECONNECT_SLEEP; // overwritten when necessary
$pools = $register->get('pools'); /** @var Group $pools */
$queue = $pools
->get('queue')
->pop()
->getResource();
if ($project !== null) {
$projectId = $project->getId();
$projectInternalId = $project->getInternalId();
}
switch ($type) {
case self::DATABASE_PROJECT:
if (!$projectId) {
throw new \Exception('ProjectID not provided - cannot get database');
}
$namespace = "_$projectInternalId";
break;
case self::DATABASE_CONSOLE:
$namespace = "_console";
$sleep = 5; // ConsoleDB needs extra sleep time to ensure tables are created
break;
default:
throw new \Exception('Unknown database type: ' . $type);
}
$attempts = 0;
while (true) {
try {
$attempts++;
$cache = new Cache(new RedisCache($register->get('cache')));
$database = new Database(new MariaDB($register->get('db')), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace); // Main DB
if (
$project === null
&& !empty($projectId)
&& !$database->getDocument('projects', $projectId)->isEmpty()
) {
throw new \Exception("Project does not exist: $projectId");
}
if ($type === self::DATABASE_CONSOLE && !$database->exists($database->getDefaultDatabase(), Database::METADATA)) {
throw new \Exception('Console project not ready');
}
break; // leave loop if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ($attempts)...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
}
return $database;
return new Usage($queue);
}
/**
@@ -285,7 +305,6 @@ abstract class Worker
return $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
}
/**
* Get Builds Storage Device
* @param string $projectId of the project
@@ -308,45 +327,83 @@ abstract class Worker
*/
public function getDevice(string $root): Device
{
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
case Storage::DEVICE_S3:
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
case Storage::DEVICE_DO_SPACES:
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
$doSpacesAcl = 'private';
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
case Storage::DEVICE_LINODE:
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
case Storage::DEVICE_WASABI:
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
$connection = App::getEnv('_APP_CONNECTIONS_STORAGE', '');
if (!empty($connection)) {
$acl = 'private';
$device = Storage::DEVICE_LOCAL;
$accessKey = '';
$accessSecret = '';
$bucket = '';
$region = '';
try {
$dsn = new DSN($connection);
$device = $dsn->getScheme();
$accessKey = $dsn->getUser() ?? '';
$accessSecret = $dsn->getPassword() ?? '';
$bucket = $dsn->getPath() ?? '';
$region = $dsn->getParam('region');
} catch (\Exception $e) {
Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.');
}
switch ($device) {
case Storage::DEVICE_S3:
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case STORAGE::DEVICE_DO_SPACES:
return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case Storage::DEVICE_BACKBLAZE:
return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case Storage::DEVICE_LINODE:
return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case Storage::DEVICE_WASABI:
return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
}
} else {
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
case Storage::DEVICE_S3:
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
case Storage::DEVICE_DO_SPACES:
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
$doSpacesAcl = 'private';
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
case Storage::DEVICE_LINODE:
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
case Storage::DEVICE_WASABI:
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
}
}
}
}
+149
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Specification;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Route;
use Appwrite\Utopia\Response\Model;
@@ -38,6 +39,17 @@ abstract class Format
'license.url' => '',
];
/*
* Blacklist to omit the enum types for the given route's parameter
*/
protected array $enumBlacklist = [
[
'namespace' => 'users',
'method' => 'getUsage',
'parameter' => 'provider'
]
];
public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount)
{
$this->app = $app;
@@ -97,4 +109,141 @@ abstract class Format
{
return $this->params[$key] ?? $default;
}
protected function getEnumName(string $service, string $method, string $param): ?string
{
switch ($service) {
case 'account':
switch ($method) {
case 'createOAuth2Session':
return 'Provider';
}
break;
case 'avatars':
switch ($method) {
case 'getBrowser':
return 'Browser';
case 'getCreditCard':
return 'CreditCard';
case 'getFlag':
return 'Flag';
}
break;
case 'storage':
switch ($method) {
case 'getFilePreview':
switch ($param) {
case 'gravity':
return 'ImageGravity';
case 'output':
return 'ImageFormat';
}
break;
}
break;
case 'databases':
switch ($method) {
case 'createRelationshipAttribute':
switch ($param) {
case 'type':
return 'RelationshipType';
case 'onDelete':
return 'RelationMutate';
}
break;
case 'updateRelationshipAttribute':
switch ($param) {
case 'onDelete':
return 'RelationMutate';
}
break;
case 'createIndex':
switch ($param) {
case 'type':
return 'IndexType';
case 'orders':
return 'OrderBy';
}
}
break;
case 'projects':
switch ($method) {
case 'createPlatform':
switch ($param) {
case 'type':
return 'PlatformType';
}
break;
}
break;
}
return null;
}
public function getEnumKeys(string $service, string $method, string $param): array
{
$values = [];
switch ($service) {
case 'avatars':
switch ($method) {
case 'getBrowser':
$codes = Config::getParam('avatar-browsers');
foreach ($codes as $code => $value) {
$values[] = $value['name'];
}
return $values;
case 'getCreditCard':
$codes = Config::getParam('avatar-credit-cards');
foreach ($codes as $code => $value) {
$values[] = $value['name'];
}
return $values;
case 'getFlag':
$codes = Config::getParam('avatar-flags');
foreach ($codes as $code => $value) {
$values[] = $value['name'];
}
return $values;
}
break;
case 'databases':
switch ($method) {
case 'getUsage':
case 'getCollectionUsage':
case 'getDatabaseUsage':
// Range Enum Keys
$values = ['Twenty Four Hours', 'Seven Days', 'Thirty Days', 'Ninety Days'];
return $values;
}
break;
case 'function':
switch ($method) {
case 'getUsage':
case 'getFunctionUsage':
// Range Enum Keys
$values = ['Twenty Four Hours', 'Seven Days', 'Thirty Days', 'Ninety Days'];
return $values;
}
break;
case 'users':
switch ($method) {
case 'getUsage':
case 'getUserUsage':
// Range Enum Keys
if ($param == 'range') {
$values = ['Twenty Four Hours', 'Seven Days', 'Thirty Days', 'Ninety Days'];
return $values;
}
}
break;
case 'storage':
switch ($method) {
case 'getUsage':
case 'getBucketUsage':
// Range Enum Keys
$values = ['Twenty Four Hours', 'Seven Days', 'Thirty Days', 'Ninety Days'];
return $values;
}
}
return $values;
}
}
+32 -1
View File
@@ -343,18 +343,23 @@ class OpenAPI3 extends Format
case 'Utopia\Validator\ArrayList':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Appwrite\Utopia\Database\Validator\Queries\Documents':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
case 'Appwrite\Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries':
$node['schema']['type'] = 'array';
$node['schema']['items'] = [
'type' => 'string',
@@ -412,6 +417,25 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = $validator->getList()[0];
//Iterate from the blackList. If it matches with the current one, then it is a blackList
// Do not add the enum
$allowed = true;
foreach ($this->enumBlacklist as $blacklist) {
if (
$blacklist['namespace'] == $route->getLabel('sdk.namespace', '')
&& $blacklist['method'] == $route->getLabel('sdk.method', '')
&& $blacklist['parameter'] == $name
) {
$allowed = false;
break;
}
}
if ($allowed) {
$node['schema']['enum'] = $validator->getList();
$node['schema']['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['schema']['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
}
if ($validator->getType() === 'integer') {
$node['format'] = 'int32';
}
@@ -442,6 +466,13 @@ class OpenAPI3 extends Format
'x-example' => $node['schema']['x-example'] ?? null
];
if (isset($node['schema']['enum'])) {
/// If the enum flag is Set, add the enum values to the body
$body['content'][$consumes[0]]['schema']['properties'][$name]['enum'] = $node['schema']['enum'];
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-name'] = $node['schema']['x-enum-name'] ?? null;
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-keys'] = $node['schema']['x-enum-keys'] ?? null;
}
if ($node['schema']['x-upload-id'] ?? false) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-upload-id'] = $node['schema']['x-upload-id'];
}
+30 -1
View File
@@ -293,6 +293,7 @@ class Swagger2 extends Format
$validator = $validator->getValidator();
}
switch ((!empty($validator)) ? \get_class($validator) : '') {
case 'Utopia\Validator\Text':
$node['type'] = $validator->getType();
@@ -342,18 +343,23 @@ class Swagger2 extends Format
case 'Utopia\Validator\ArrayList':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Appwrite\Utopia\Database\Validator\Queries\Documents':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
case 'Appwrite\Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries':
$node['type'] = 'array';
$node['collectionFormat'] = 'multi';
$node['items'] = [
@@ -414,6 +420,22 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
$node['x-example'] = $validator->getList()[0];
//Iterate from the blackList. If it matches with the current one, then it is a blackList
// Do not add the enum
$allowed = true;
foreach ($this->enumBlacklist as $blacklist) {
if ($blacklist['namespace'] == $route->getLabel('sdk.namespace', '') && $blacklist['method'] == $route->getLabel('sdk.method', '') && $blacklist['parameter'] == $name) {
$allowed = false;
break;
}
}
if ($allowed) {
$node['enum'] = $validator->getList();
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
}
if ($validator->getType() === 'integer') {
$node['format'] = 'int32';
}
@@ -452,6 +474,13 @@ class Swagger2 extends Format
'x-example' => $node['x-example'] ?? null,
];
if (isset($node['enum'])) {
/// If the enum flag is Set, add the enum values to the body
$body['schema']['properties'][$name]['enum'] = $node['enum'];
$body['schema']['properties'][$name]['x-enum-name'] = $node['x-enum-name'] ?? null;
$body['schema']['properties'][$name]['x-enum-keys'] = $node['x-enum-keys'] ?? null;
}
if ($node['x-global'] ?? false) {
$body['schema']['properties'][$name]['x-global'] = true;
}
+28 -10
View File
@@ -8,6 +8,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use InfluxDB\Database as InfluxDatabase;
use DateTime;
use Utopia\Registry\Registry;
class TimeSeries extends Calculator
{
@@ -32,6 +33,20 @@ class TimeSeries extends Calculator
*/
protected $errorHandler;
/**
* Callback to get project DB
*
* @var callable
*/
protected mixed $getProjectDB;
/**
* Registry
*
* @var Registry
*/
protected Registry $register;
/**
* Latest times for metric that was synced to the database
*
@@ -382,11 +397,13 @@ class TimeSeries extends Calculator
]
];
public function __construct(string $region, Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
public function __construct(string $region, Database $database, InfluxDatabase $influxDB, callable $getProjectDB, Registry $register, callable $errorHandler = null)
{
parent::__construct($region);
$this->database = $database;
$this->influxDB = $influxDB;
$this->getProjectDB = $getProjectDB;
$this->register = $register;
$this->errorHandler = $errorHandler;
}
@@ -406,12 +423,13 @@ class TimeSeries extends Calculator
private function createOrUpdateMetric(string $projectId, string $time, string $period, string $metric, int $value, int $type): void
{
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_' . $projectId);
$project = $this->database->getDocument('projects', $projectId);
$database = call_user_func($this->getProjectDB, $project);
try {
$document = $this->database->getDocument('stats', $id);
$document = $database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
$database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
@@ -421,7 +439,7 @@ class TimeSeries extends Calculator
'region' => $this->region,
]));
} else {
$this->database->updateDocument(
$database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
@@ -434,6 +452,8 @@ class TimeSeries extends Calculator
throw $e;
}
}
$this->register->get('pools')->reclaim();
}
/**
@@ -469,7 +489,7 @@ class TimeSeries extends Calculator
$query .= "WHERE \"time\" > '{$start}' ";
$query .= "AND \"time\" < '{$end}' ";
$query .= "AND \"metric_type\"='counter' {$filters} ";
$query .= "GROUP BY time({$period['key']}), \"projectId\", \"projectInternalId\" {$groupBy} ";
$query .= "GROUP BY time({$period['key']}), \"projectId\" {$groupBy} ";
$query .= "FILL(null)";
try {
@@ -491,11 +511,9 @@ class TimeSeries extends Calculator
}
$value = (!empty($point['value'])) ? $point['value'] : 0;
if (empty($point['projectInternalId'] ?? null)) {
continue;
}
$this->createOrUpdateMetric(
$point['projectInternalId'],
$point['projectId'],
$point['time'],
$period['key'],
$metricUpdated,
@@ -1,111 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class IndexedQueries extends Queries
{
/**
* @var array<Document>
*/
protected array $attributes = [];
/**
* @var array<Document>
*/
protected array $indexes = [];
/**
* Expression constructor
*
* This Queries Validator filters indexes for only available indexes
*
* @param array<Document> $attributes
* @param array<Document> $indexes
* @param Base ...$validators
* @throws \Exception
*/
public function __construct(array $attributes = [], array $indexes = [], Base ...$validators)
{
$this->attributes = $attributes;
$this->indexes[] = new Document([
'type' => Database::INDEX_UNIQUE,
'attributes' => ['$id']
]);
$this->indexes[] = new Document([
'type' => Database::INDEX_KEY,
'attributes' => ['$createdAt']
]);
$this->indexes[] = new Document([
'type' => Database::INDEX_KEY,
'attributes' => ['$updatedAt']
]);
foreach ($indexes ?? [] as $index) {
$this->indexes[] = $index;
}
parent::__construct(...$validators);
}
/**
* Is valid.
*
* Returns false if:
* 1. any query in $value is invalid based on $validator
* 2. there is no index with an exact match of the filters
* 3. there is no index with an exact match of the order attributes
*
* Otherwise, returns true.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (!parent::isValid($value)) {
return false;
}
$queries = [];
foreach ($value as $query) {
if (!$query instanceof Query) {
$query = Query::parse($query);
}
$queries[] = $query;
}
$grouped = Query::groupByType($queries);
$filters = $grouped['filters'];
foreach ($filters as $filter) {
if ($filter->getMethod() === Query::TYPE_SEARCH) {
$matched = false;
foreach ($this->indexes as $index) {
if (
$index->getAttribute('type') === Database::INDEX_FULLTEXT
&& $index->getAttribute('attributes') === [$filter->getAttribute()]
) {
$matched = true;
}
}
if (!$matched) {
$this->message = "Searching by attribute \"{$filter->getAttribute()}\" requires a fulltext index.";
return false;
}
}
}
return true;
}
}
@@ -1,26 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Document;
use Utopia\Database\Validator\OrderAttributes as ValidatorOrderAttributes;
class OrderAttributes extends ValidatorOrderAttributes
{
/**
* Expression constructor
*
* @param Document[] $attributes
* @param Document[] $indexes
* @param bool $strict
*/
public function __construct($attributes, $indexes, $strict)
{
// Remove failed/stuck/processing indexes
$indexes = \array_filter($indexes, function ($index) {
return $index->getAttribute('status') === 'available';
});
parent::__construct($attributes, $indexes, $strict);
}
}
@@ -0,0 +1,56 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Validator;
class ProjectId extends Validator
{
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($value): bool
{
return $value == 'unique()' || preg_match('/^[a-z0-9][a-z0-9-]{1,35}$/', $value);
}
/**
* Get description.
*
* @return string
*/
public function getDescription(): string
{
return 'Project IDs must contain at most 36 chars. Valid chars are a-z, 0-9, and hyphen. Can\'t start with a special char.';
}
/**
* 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_STRING;
}
}
@@ -1,135 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Validator;
use Utopia\Database\Query;
class Queries extends Validator
{
/**
* @var string
*/
protected string $message = 'Invalid queries';
/**
* @var array<Base>
*/
protected array $validators;
/**
* Queries constructor
*
* @param Base ...$validators a list of validators
*/
public function __construct(Base ...$validators)
{
$this->validators = $validators;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns false if:
* 1. any query in $value is invalid based on $validator
*
* Otherwise, returns true.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
foreach ($value as $query) {
if (!$query instanceof Query) {
try {
$query = Query::parse($query);
} catch (\Throwable) {
$this->message = "Invalid query: {$query}";
return false;
}
}
$method = $query->getMethod();
$methodType = match ($method) {
Query::TYPE_SELECT => Base::METHOD_TYPE_SELECT,
Query::TYPE_LIMIT => Base::METHOD_TYPE_LIMIT,
Query::TYPE_OFFSET => Base::METHOD_TYPE_OFFSET,
Query::TYPE_CURSORAFTER,
Query::TYPE_CURSORBEFORE => Base::METHOD_TYPE_CURSOR,
Query::TYPE_ORDERASC,
Query::TYPE_ORDERDESC => Base::METHOD_TYPE_ORDER,
Query::TYPE_EQUAL,
Query::TYPE_NOTEQUAL,
Query::TYPE_LESSER,
Query::TYPE_LESSEREQUAL,
Query::TYPE_GREATER,
Query::TYPE_GREATEREQUAL,
Query::TYPE_SEARCH,
Query::TYPE_IS_NULL,
Query::TYPE_IS_NOT_NULL,
Query::TYPE_BETWEEN,
Query::TYPE_STARTS_WITH,
Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER,
default => '',
};
$methodIsValid = false;
foreach ($this->validators as $validator) {
if ($validator->getMethodType() !== $methodType) {
continue;
}
if (!$validator->isValid($query)) {
$this->message = 'Query not valid: ' . $validator->getDescription();
return false;
}
$methodIsValid = true;
}
if (!$methodIsValid) {
$this->message = 'Query method not valid: ' . $method;
return false;
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return true;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Select;
class Attributes extends Base
{
public const ALLOWED_ATTRIBUTES = [
'key',
'type',
'size',
'required',
'array',
'status',
'error'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('attributes', self::ALLOWED_ATTRIBUTES);
}
}
@@ -2,13 +2,13 @@
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Appwrite\Extend\Exception;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Filter;
use Utopia\Database\Validator\Query\Order;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -24,7 +24,9 @@ class Base extends Queries
*/
public function __construct(string $collection, array $allowedAttributes)
{
$collection = Config::getParam('collections', [])[$collection];
$config = Config::getParam('collections', []);
$collections = array_merge($config['console'], $config['projects'], $config['buckets'], $config['databases']);
$collection = $collections[$collection];
// array for constant lookup time
$allowedAttributesLookup = [];
foreach ($allowedAttributes as $attribute) {
@@ -67,9 +69,8 @@ class Base extends Queries
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct(...$validators);
parent::__construct($validators);
}
}
@@ -5,10 +5,11 @@ namespace Appwrite\Utopia\Database\Validator\Queries;
class Deployments extends Base
{
public const ALLOWED_ATTRIBUTES = [
'entrypoint',
'size',
'buildId',
'activate',
'entrypoint',
'commands'
];
/**
@@ -1,41 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
class Document extends Queries
{
/**
* Expression constructor
*
* @param array $attributes
* @throws \Exception
*/
public function __construct(array $attributes)
{
$attributes[] = new \Utopia\Database\Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Select($attributes),
];
parent::__construct(...$validators);
}
}
@@ -1,52 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
use Utopia\Database\Document;
class Documents extends IndexedQueries
{
/**
* Expression constructor
*
* @param Document[] $attributes
* @throws \Exception
*/
public function __construct(array $attributes, array $indexes)
{
$attributes[] = new Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Limit(),
new Offset(),
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct($attributes, $indexes, ...$validators);
}
}
@@ -7,7 +7,7 @@ class Executions extends Base
public const ALLOWED_ATTRIBUTES = [
'trigger',
'status',
'statusCode',
'responseStatusCode',
'duration'
];
@@ -12,7 +12,10 @@ class Functions extends Base
'schedule',
'scheduleNext',
'schedulePrevious',
'timeout'
'timeout',
'entrypoint',
'commands',
'installationId'
];
/**
@@ -0,0 +1,23 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Identities extends Base
{
public const ALLOWED_ATTRIBUTES = [
'userId',
'provider',
'providerUid',
'providerEmail',
'providerAccessTokenExpiry',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('identities', self::ALLOWED_ATTRIBUTES);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Indexes extends Base
{
public const ALLOWED_ATTRIBUTES = [
'key',
'type',
'status',
'attributes',
'error',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('indexes', self::ALLOWED_ATTRIBUTES);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Installations extends Base
{
public const ALLOWED_ATTRIBUTES = [
'provider',
'organization'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('installations', self::ALLOWED_ATTRIBUTES);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Migrations extends Base
{
public const ALLOWED_ATTRIBUTES = [
'status',
'stage',
'source',
'resources',
'statusCounters',
'resourceData',
'errors'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('migrations', self::ALLOWED_ATTRIBUTES);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Rules extends Base
{
public const ALLOWED_ATTRIBUTES = [
'domain',
'resourceType',
'resourceId',
'url'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('rules', self::ALLOWED_ATTRIBUTES);
}
}
@@ -7,7 +7,9 @@ use Appwrite\Utopia\Database\Validator\Queries\Base;
class Variables extends Base
{
public const ALLOWED_ATTRIBUTES = [
'key'
'key',
'resourceType',
'resourceId'
];
/**
@@ -1,62 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Validator;
use Utopia\Database\Query;
abstract class Base extends Validator
{
public const METHOD_TYPE_LIMIT = 'limit';
public const METHOD_TYPE_OFFSET = 'offset';
public const METHOD_TYPE_CURSOR = 'cursor';
public const METHOD_TYPE_ORDER = 'order';
public const METHOD_TYPE_FILTER = 'filter';
public const METHOD_TYPE_SELECT = 'select';
/**
* @var string
*/
protected $message = 'Invalid query';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* 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;
}
/**
* Returns what type of query this Validator is for
*/
abstract public function getMethodType(): string;
}
@@ -1,44 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
class Cursor extends Base
{
/**
* Is valid.
*
* Returns true if method is cursorBefore or cursorAfter and value is not null
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method === Query::TYPE_CURSORAFTER || $method === Query::TYPE_CURSORBEFORE) {
$cursor = $query->getValue();
$validator = new UID();
if ($validator->isValid($cursor)) {
return true;
}
$this->message = 'Invalid cursor: ' . $validator->getDescription();
return false;
}
return false;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_CURSOR;
}
}
@@ -1,141 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Database;
use Utopia\Database\Query;
class Filter extends Base
{
/**
* @var string
*/
protected $message = 'Invalid query';
/**
* @var array
*/
protected $schema = [];
private int $maxValuesCount;
/**
* Query constructor
*
* @param int $maxValuesCount
*/
public function __construct(array $attributes = [], int $maxValuesCount = 100)
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
$this->maxValuesCount = $maxValuesCount;
}
protected function isValidAttribute($attribute): bool
{
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
// TODO: Remove this when nested queries are supported
if (isset($this->schema[$attribute])) {
$this->message = 'Cannot query nested attribute on: ' . $attribute;
return false;
}
}
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
return true;
}
protected function isValidAttributeAndValues(string $attribute, array $values): bool
{
if (!$this->isValidAttribute($attribute)) {
return false;
}
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
$attributeSchema = $this->schema[$attribute];
if (count($values) > $this->maxValuesCount) {
$this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute;
return false;
}
// Extract the type of desired attribute from collection $schema
$attributeType = $attributeSchema['type'];
foreach ($values as $value) {
$condition = match ($attributeType) {
Database::VAR_RELATIONSHIP => true,
Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING,
Database::VAR_FLOAT => (gettype($value) === Database::VAR_FLOAT || gettype($value) === Database::VAR_INTEGER),
default => gettype($value) === $attributeType
};
if (!$condition) {
$this->message = 'Query type does not match expected: ' . $attributeType;
return false;
}
}
return true;
}
/**
* Is valid.
*
* Returns true if method is a filter method, attribute exists, and value matches attribute type
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
$attribute = $query->getAttribute();
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_NOTEQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSEREQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_BETWEEN:
case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
$values = $query->getValues();
return $this->isValidAttributeAndValues($attribute, $values);
default:
return false;
}
}
public function getMethodType(): string
{
return self::METHOD_TYPE_FILTER;
}
}
@@ -1,61 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator\Range;
class Limit extends Base
{
protected int $maxLimit;
/**
* Query constructor
*
* @param int $maxLimit
*/
public function __construct(int $maxLimit = PHP_INT_MAX)
{
$this->maxLimit = $maxLimit;
}
protected function isValidLimit($limit): bool
{
$validator = new Range(0, $this->maxLimit);
if ($validator->isValid($limit)) {
return true;
}
$this->message = 'Invalid limit: ' . $validator->getDescription();
return false;
}
/**
* Is valid.
*
* Returns true if method is limit values are within range.
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method !== Query::TYPE_LIMIT) {
$this->message = 'Query method invalid: ' . $method;
return false;
}
$limit = $query->getValue();
return $this->isValidLimit($limit);
}
public function getMethodType(): string
{
return self::METHOD_TYPE_LIMIT;
}
}
@@ -1,60 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Database\Query;
use Utopia\Validator\Range;
class Offset extends Base
{
protected int $maxOffset;
/**
* Query constructor
*
* @param int $maxOffset
*/
public function __construct(int $maxOffset = PHP_INT_MAX)
{
$this->maxOffset = $maxOffset;
}
protected function isValidOffset($offset): bool
{
$validator = new Range(0, $this->maxOffset);
if ($validator->isValid($offset)) {
return true;
}
$this->message = 'Invalid offset: ' . $validator->getDescription();
return false;
}
/**
* Is valid.
*
* Returns true if method is offset and values are within range.
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method !== Query::TYPE_OFFSET) {
$this->message = 'Query method invalid: ' . $method;
return false;
}
$offset = $query->getValue();
return $this->isValidOffset($offset);
}
public function getMethodType(): string
{
return self::METHOD_TYPE_OFFSET;
}
}
@@ -1,68 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator;
class Order extends Base
{
/**
* @var array
*/
protected $schema = [];
/**
* Query constructor
*
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
}
protected function isValidAttribute($attribute): bool
{
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
return true;
}
/**
* Is valid.
*
* Returns true if method is ORDER_ASC or ORDER_DESC and attributes are valid
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
$method = $query->getMethod();
$attribute = $query->getAttribute();
if ($method === Query::TYPE_ORDERASC || $method === Query::TYPE_ORDERDESC) {
if ($attribute === '') {
return true;
}
return $this->isValidAttribute($attribute);
}
return false;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_ORDER;
}
}
@@ -1,60 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class Select extends Base
{
protected array $schema = [];
/**
* Query constructor
*
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
}
/**
* Is valid.
*
* Returns true if method is TYPE_SELECT selections are valid
*
* Otherwise, returns false
*
* @param $query
* @return bool
*/
public function isValid($query): bool
{
/* @var $query Query */
if ($query->getMethod() !== Query::TYPE_SELECT) {
return false;
}
foreach ($query->getValues() as $attribute) {
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
if (!isset($this->schema[$attribute]) && $attribute !== '*') {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
}
return true;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_SELECT;
}
}
+3 -3
View File
@@ -200,11 +200,11 @@ class V15 extends Filter
$operations = [
'equal' => Query::TYPE_EQUAL,
'notEqual' => Query::TYPE_NOTEQUAL,
'notEqual' => Query::TYPE_NOT_EQUAL,
'lesser' => Query::TYPE_LESSER,
'lesserEqual' => Query::TYPE_LESSEREQUAL,
'lesserEqual' => Query::TYPE_LESSER_EQUAL,
'greater' => Query::TYPE_GREATER,
'greaterEqual' => Query::TYPE_GREATEREQUAL,
'greaterEqual' => Query::TYPE_GREATER_EQUAL,
'search' => Query::TYPE_SEARCH,
];
foreach ($content['queries'] as $i => $query) {
@@ -0,0 +1,48 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
class V16 extends Filter
{
// Convert 1.3 params to 1.4
public function parse(array $content, string $model): array
{
switch ($model) {
case 'functions.create':
$content['commands'] = $this->getCommands($content['runtime'] ?? '');
break;
case 'functions.update':
$content['commands'] = $this->getCommands($content['runtime'] ?? '');
break;
case 'functions.createExecution':
$content['body'] = $content['data'];
unset($content['data']);
break;
}
return $content;
}
private function getCommands(string $runtime): string
{
if (\str_starts_with($runtime, 'node')) {
return 'npm install';
} elseif (\str_starts_with($runtime, 'python')) {
return 'pip install --no-cache-dir -r requirements.txt';
} elseif (\str_starts_with($runtime, 'dart')) {
return 'dart pub get';
} elseif (\str_starts_with($runtime, 'php')) {
return 'composer update --no-interaction --ignore-platform-reqs --optimize-autoloader --prefer-dist --no-dev';
} elseif (\str_starts_with($runtime, 'ruby')) {
return 'bundle install';
} elseif (\str_starts_with($runtime, 'swift')) {
return 'swift package resolve';
} elseif (\str_starts_with($runtime, 'dotnet')) {
return 'dotnet restore';
}
return '';
}
}
+85 -11
View File
@@ -32,6 +32,7 @@ use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Database;
use Appwrite\Utopia\Response\Model\Continent;
@@ -47,6 +48,7 @@ use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -64,6 +66,9 @@ use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\Detection;
use Appwrite\Utopia\Response\Model\Headers;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Model\Preferences;
@@ -72,9 +77,12 @@ use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Mock; // Keep last
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
@@ -85,6 +93,11 @@ use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
// Keep last
use Appwrite\Utopia\Response\Model\Mock;
/**
* @method int getStatusCode()
@@ -142,6 +155,8 @@ class Response extends SwooleResponse
public const MODEL_USER_LIST = 'userList';
public const MODEL_SESSION = 'session';
public const MODEL_SESSION_LIST = 'sessionList';
public const MODEL_IDENTITY = 'identity';
public const MODEL_IDENTITY_LIST = 'identityList';
public const MODEL_TOKEN = 'token';
public const MODEL_JWT = 'jwt';
public const MODEL_PREFERENCES = 'preferences';
@@ -163,6 +178,8 @@ class Response extends SwooleResponse
// Locale
public const MODEL_LOCALE = 'locale';
public const MODEL_LOCALE_CODE = 'localeCode';
public const MODEL_LOCALE_CODE_LIST = 'localeCodeList';
public const MODEL_COUNTRY = 'country';
public const MODEL_COUNTRY_LIST = 'countryList';
public const MODEL_CONTINENT = 'continent';
@@ -180,6 +197,15 @@ class Response extends SwooleResponse
public const MODEL_MEMBERSHIP = 'membership';
public const MODEL_MEMBERSHIP_LIST = 'membershipList';
// VCS
public const MODEL_INSTALLATION = 'installation';
public const MODEL_INSTALLATION_LIST = 'installationList';
public const MODEL_PROVIDER_REPOSITORY = 'providerRepository';
public const MODEL_PROVIDER_REPOSITORY_LIST = 'providerRepositoryList';
public const MODEL_BRANCH = 'branch';
public const MODEL_BRANCH_LIST = 'branchList';
public const MODEL_DETECTION = 'detection';
// Functions
public const MODEL_FUNCTION = 'function';
public const MODEL_FUNCTION_LIST = 'functionList';
@@ -192,6 +218,18 @@ class Response extends SwooleResponse
public const MODEL_BUILD = 'build';
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
public const MODEL_HEADERS = 'headers';
// Proxy
public const MODEL_PROXY_RULE = 'proxyRule';
public const MODEL_PROXY_RULE_LIST = 'proxyRuleList';
// Migrations
public const MODEL_MIGRATION = 'migration';
public const MODEL_MIGRATION_LIST = 'migrationList';
public const MODEL_MIGRATION_REPORT = 'migrationReport';
public const MODEL_MIGRATION_FIREBASE_PROJECT = 'firebaseProject';
public const MODEL_MIGRATION_FIREBASE_PROJECT_LIST = 'firebaseProjectList';
// Project
public const MODEL_PROJECT = 'project';
@@ -204,10 +242,11 @@ class Response extends SwooleResponse
public const MODEL_PROVIDER_LIST = 'providerList';
public const MODEL_PLATFORM = 'platform';
public const MODEL_PLATFORM_LIST = 'platformList';
public const MODEL_DOMAIN = 'domain';
public const MODEL_DOMAIN_LIST = 'domainList';
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
public const MODEL_VCS = 'vcs';
public const MODEL_SMS_TEMPLATE = 'smsTemplate';
public const MODEL_EMAIL_TEMPLATE = 'emailTemplate';
// Health
public const MODEL_HEALTH_STATUS = 'healthStatus';
@@ -215,6 +254,7 @@ class Response extends SwooleResponse
public const MODEL_HEALTH_QUEUE = 'healthQueue';
public const MODEL_HEALTH_TIME = 'healthTime';
public const MODEL_HEALTH_ANTIVIRUS = 'healthAntivirus';
public const MODEL_HEALTH_STATUS_LIST = 'healthStatusList';
// Console
public const MODEL_CONSOLE_VARIABLES = 'consoleVariables';
@@ -223,6 +263,8 @@ class Response extends SwooleResponse
public const MODEL_PERMISSIONS = 'permissions';
public const MODEL_RULE = 'rule';
public const MODEL_TASK = 'task';
public const MODEL_DOMAIN = 'domain';
public const MODEL_DOMAIN_LIST = 'domainList';
// Tests (keep last)
public const MODEL_MOCK = 'mock';
@@ -257,12 +299,16 @@ class Response extends SwooleResponse
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG))
->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE))
->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET))
->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('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))
->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME))
->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT))
->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION))
@@ -272,7 +318,6 @@ class Response extends SwooleResponse
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
->setModel(new BaseList('Providers List', self::MODEL_PROVIDER_LIST, 'platforms', self::MODEL_PROVIDER, true, false))
->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false))
->setModel(new BaseList('Domains List', self::MODEL_DOMAIN_LIST, 'domains', self::MODEL_DOMAIN, true, false))
->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY))
->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT))
->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE))
@@ -280,6 +325,11 @@ class Response extends SwooleResponse
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
// Entities
->setModel(new Database())
->setModel(new Collection())
@@ -309,14 +359,20 @@ class Response extends SwooleResponse
->setModel(new Account())
->setModel(new Preferences())
->setModel(new Session())
->setModel(new Identity())
->setModel(new Token())
->setModel(new JWT())
->setModel(new Locale())
->setModel(new LocaleCode())
->setModel(new File())
->setModel(new Bucket())
->setModel(new Team())
->setModel(new Membership())
->setModel(new Func())
->setModel(new Installation())
->setModel(new ProviderRepository())
->setModel(new Detection())
->setModel(new Branch())
->setModel(new Runtime())
->setModel(new Deployment())
->setModel(new Execution())
@@ -324,7 +380,6 @@ class Response extends SwooleResponse
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
->setModel(new Domain())
->setModel(new Provider())
->setModel(new Platform())
->setModel(new Variable())
@@ -348,7 +403,14 @@ class Response extends SwooleResponse
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new Headers())
->setModel(new Rule())
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())
// Verification
// Recovery
// Tests (keep last)
@@ -418,7 +480,7 @@ class Response extends SwooleResponse
*/
public function dynamic(Document $document, string $model): void
{
$output = $this->output($document, $model);
$output = $this->output(clone $document, $model);
// If filter is set, parse the output
if (self::hasFilter()) {
@@ -459,22 +521,22 @@ class Response extends SwooleResponse
*/
public function output(Document $document, string $model): array
{
$data = $document;
$data = clone $document;
$model = $this->getModel($model);
$output = [];
$document = $model->filter($document);
$data = $model->filter($data);
if ($model->isAny()) {
$this->payload = $document->getArrayCopy();
$this->payload = $data->getArrayCopy();
return $this->payload;
}
foreach ($model->getRules() as $key => $rule) {
if (!$document->isSet($key) && $rule['required']) { // do not set attribute in response if not required
if (!$data->isSet($key) && $rule['required']) { // do not set attribute in response if not required
if (\array_key_exists('default', $rule)) {
$document->setAttribute($key, $rule['default']);
$data->setAttribute($key, $rule['default']);
} else {
throw new Exception('Model ' . $model->getName() . ' is missing response key: ' . $key);
}
@@ -607,4 +669,16 @@ class Response extends SwooleResponse
{
return self::$filter != null;
}
/**
* Set Header
*
* @param string $key
* @param string $value
* @return void
*/
public function setHeader(string $key, string $value): void
{
$this->sendHeader($key, $value);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
use Cron\CronExpression;
use Utopia\Database\DateTime;
class V16 extends Filter
{
// Convert 1.4 Data format to 1.3 format
public function parse(array $content, string $model): array
{
$parsedResponse = $content;
switch ($model) {
case Response::MODEL_DEPLOYMENT:
$parsedResponse = $this->parseDeployment($parsedResponse);
break;
case Response::MODEL_PROXY_RULE:
// We won't be supporting the domain endpoints for older SDKs
// since these APIs are internal. As such, no filtering required
break;
case Response::MODEL_EXECUTION:
$parsedResponse = $this->parseExecution($parsedResponse);
break;
case Response::MODEL_FUNCTION:
$parsedResponse = $this->parseFunction($parsedResponse);
break;
case Response::MODEL_PROJECT:
$parsedResponse = $this->parseProject($parsedResponse);
break;
case Response::MODEL_VARIABLE:
$parsedResponse = $this->parseVariable($parsedResponse);
break;
}
return $parsedResponse;
}
protected function parseDeployment(array $content)
{
$content['buildStderr'] = '';
$content['buildStdout'] = $content['buildLogs'];
unset($content['buildLogs']);
return $content;
}
protected function parseExecution(array $content)
{
if (isset($content['responseStatusCode'])) {
$content['statusCode'] = $content['responseStatusCode'];
unset($content['responseStatusCode']);
}
if (isset($content['responseBody'])) {
$content['response'] = $content['responseBody'];
unset($content['responseBody']);
}
if (isset($content['logs'])) {
$content['stdout'] = $content['logs'];
unset($content['logs']);
}
if (isset($content['errors'])) {
$content['stderr'] = $content['errors'];
unset($content['errors']);
}
return $content;
}
protected function parseFunction(array $content)
{
$content['schedulePrevious'] = '';
$content['scheduleNext'] = '';
if (!empty($content['schedule'])) {
$cron = new CronExpression($content['schedule']);
$content['schedulePrevious'] = DateTime::formatTz(DateTime::format($cron->getPreviousRunDate()));
$content['scheduleNext'] = DateTime::formatTz(DateTime::format($cron->getNextRunDate()));
}
return $content;
}
protected function parseProject(array $content)
{
foreach ($content['providers'] ?? [] as $i => $provider) {
$content['providers'][$i]['name'] = \ucfirst($provider['key']);
unset($content['providers'][$i]['key']);
}
$content['domains'] = [];
return $content;
}
protected function parseVariable(array $content)
{
if (isset($content['resourceId'])) {
$content['functionId'] = $content['resourceId'];
unset($content['resourceId']);
}
return $content;
}
}
@@ -28,6 +28,12 @@ class Attribute extends Model
'default' => '',
'example' => 'available',
])
->addRule('error', [
'type' => self::TYPE_STRING,
'description' => 'Error message. Displays error generated on failure of creating or deleting an attribute.',
'default' => '',
'example' => 'string',
])
->addRule('required', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is attribute required?',
@@ -3,6 +3,7 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class AttributeRelationship extends Attribute
{
@@ -73,4 +74,23 @@ class AttributeRelationship extends Attribute
{
return Response::MODEL_ATTRIBUTE_RELATIONSHIP;
}
/**
* Process Document before returning it to the client
*
* @return Document
*/
public function filter(Document $document): Document
{
$options = $document->getAttribute('options');
if (!\is_null($options)) {
$document->setAttribute('relatedCollection', $options['relatedCollection']);
$document->setAttribute('relationType', $options['relationType']);
$document->setAttribute('twoWay', $options['twoWay']);
$document->setAttribute('twoWayKey', $options['twoWayKey']);
$document->setAttribute('side', $options['side']);
$document->setAttribute('onDelete', $options['onDelete']);
}
return $document;
}
}
@@ -34,7 +34,7 @@ class BaseList extends Model
$namesWithCap = [
'documents', 'collections', 'users', 'files', 'buckets', 'functions',
'deployments', 'executions', 'projects', 'webhooks', 'keys',
'platforms', 'domains', 'memberships', 'teams'
'platforms', 'rules', 'memberships', 'teams'
];
if (\in_array($name, $namesWithCap)) {
@@ -0,0 +1,40 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Branch extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Branch Name.',
'default' => '',
'example' => 'main',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Branch';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_BRANCH;
}
}
@@ -63,6 +63,12 @@ class Build extends Model
'default' => 0,
'example' => 0,
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'The code size in bytes.',
'default' => 0,
'example' => 128,
])
;
}
@@ -49,7 +49,7 @@ class Collection extends Model
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Collection enabled.',
'description' => 'Collection enabled. Can be \'enabled\' or \'disabled\'. When disabled, the collection is inaccessible to users, but remains accessible to Server SDKs using API keys.',
'default' => true,
'example' => false,
])
@@ -33,6 +33,24 @@ class ConsoleVariables extends Model
'description' => 'Defines if usage stats are enabled. This value is set to \'enabled\' by default, to disable the usage stats set the value to \'disabled\'.',
'default' => '',
'example' => 'enabled',
])
->addRule('_APP_VCS_ENABLED', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Defines if VCS (Version Control System) is enabled.',
'default' => false,
'example' => true,
])
->addRule('_APP_DOMAIN_ENABLED', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Defines if main domain is configured. If so, custom domains can be created.',
'default' => false,
'example' => true,
])
->addRule('_APP_ASSISTANT_ENABLED', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Defines if AI assistant is enabled.',
'default' => false,
'example' => true,
]);
}
@@ -36,7 +36,7 @@ class Database extends Model
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Database enabled.',
'description' => 'If database is enabled. Can be \'enabled\' or \'disabled\'. When disabled, the database is inaccessible to users, but remains accessible to Server SDKs using API keys.',
'default' => true,
'example' => false,
])
@@ -28,6 +28,12 @@ class Deployment extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Type of deployment.',
'default' => '',
'example' => 'vcs',
])
->addRule('resourceId', [
'type' => self::TYPE_STRING,
'description' => 'Resource ID.',
@@ -44,7 +50,7 @@ class Deployment extends Model
'type' => self::TYPE_STRING,
'description' => 'The entrypoint file to use to execute the deployment code.',
'default' => '',
'example' => 'enabled',
'example' => 'index.js',
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
@@ -66,21 +72,15 @@ class Deployment extends Model
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'The deployment status. Possible values are "processing", "building", "pending", "ready", and "failed".',
'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".',
'default' => '',
'example' => 'ready',
])
->addRule('buildStdout', [
->addRule('buildLogs', [
'type' => self::TYPE_STRING,
'description' => 'The build stdout.',
'description' => 'The build logs.',
'default' => '',
'example' => 'enabled',
])
->addRule('buildStderr', [
'type' => self::TYPE_STRING,
'description' => 'The build stderr.',
'default' => '',
'example' => 'enabled',
'example' => 'Compiling source files...',
])
->addRule('buildTime', [
'type' => self::TYPE_INTEGER,
@@ -88,7 +88,72 @@ class Deployment extends Model
'default' => 0,
'example' => 128,
])
;
->addRule('providerRepositoryName', [
'type' => self::TYPE_STRING,
'description' => 'The name of the vcs provider repository',
'default' => '',
'example' => 'database',
])
->addRule('providerRepositoryOwner', [
'type' => self::TYPE_STRING,
'description' => 'The name of the vcs provider repository owner',
'default' => '',
'example' => 'utopia',
])
->addRule('providerRepositoryUrl', [
'type' => self::TYPE_STRING,
'description' => 'The url of the vcs provider repository',
'default' => '',
'example' => 'https://github.com/vermakhushboo/g4-node-function',
])
->addRule('providerBranch', [
'type' => self::TYPE_STRING,
'description' => 'The branch name of the vcs provider repository',
'default' => '',
'example' => 'main',
])
->addRule('providerCommitHash', [
'type' => self::TYPE_STRING,
'description' => 'The commit hash of the vcs commit',
'default' => '',
'example' => '7c3f25d',
])
->addRule('providerCommitAuthorUrl', [
'type' => self::TYPE_STRING,
'description' => 'The url of vcs commit author',
'default' => '',
'example' => 'https://github.com/vermakhushboo',
])
->addRule('providerCommitAuthor', [
'type' => self::TYPE_STRING,
'description' => 'The name of vcs commit author',
'default' => '',
'example' => 'Khushboo Verma',
])
->addRule('providerCommitMessage', [
'type' => self::TYPE_STRING,
'description' => 'The commit message',
'default' => '',
'example' => 'Update index.js',
])
->addRule('providerCommitUrl', [
'type' => self::TYPE_STRING,
'description' => 'The url of the vcs commit',
'default' => '',
'example' => 'https://github.com/vermakhushboo/g4-node-function/commit/60c0416257a9cbcdd96b2d370c38d8f8d150ccfb',
])
->addRule('providerBranch', [
'type' => self::TYPE_STRING,
'description' => 'The branch of the vcs repository',
'default' => '',
'example' => '0.7.x',
])
->addRule('providerBranchUrl', [
'type' => self::TYPE_STRING,
'description' => 'The branch of the vcs repository',
'default' => '',
'example' => 'https://github.com/vermakhushboo/appwrite/tree/0.7.x',
]);
}
/**
@@ -0,0 +1,40 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Detection extends Model
{
public function __construct()
{
$this
->addRule('runtime', [
'type' => self::TYPE_STRING,
'description' => 'Runtime',
'default' => '',
'example' => 'node',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Detection';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_DETECTION;
}
}
@@ -1,88 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Domain extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Domain ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Domain creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Domain update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('domain', [
'type' => self::TYPE_STRING,
'description' => 'Domain name.',
'default' => '',
'example' => 'appwrite.company.com',
])
->addRule('registerable', [
'type' => self::TYPE_STRING,
'description' => 'Registerable domain name.',
'default' => '',
'example' => 'company.com',
])
->addRule('tld', [
'type' => self::TYPE_STRING,
'description' => 'TLD name.',
'default' => '',
'example' => 'com',
])
->addRule('verification', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Verification process status.',
'default' => false,
'example' => true,
])
->addRule('certificateId', [
'type' => self::TYPE_STRING,
'description' => 'Certificate ID.',
'default' => '',
'example' => '6ejea5c13377e',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Domain';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_DOMAIN;
}
}
@@ -54,33 +54,59 @@ class Execution extends Model
'default' => '',
'example' => 'processing',
])
->addRule('statusCode', [
->addRule('requestMethod', [
'type' => self::TYPE_STRING,
'description' => 'HTTP request method type.',
'default' => '',
'example' => 'GET',
])
->addRule('requestPath', [
'type' => self::TYPE_STRING,
'description' => 'HTTP request path and query.',
'default' => '',
'example' => '/articles?id=5',
])
->addRule('requestHeaders', [
'type' => Response::MODEL_HEADERS,
'description' => 'HTTP response headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous.',
'default' => [],
'example' => [['Content-Type' => 'application/json']],
'array' => true,
])
->addRule('responseStatusCode', [
'type' => self::TYPE_INTEGER,
'description' => 'The script status code.',
'description' => 'HTTP response status code.',
'default' => 0,
'example' => 0,
'example' => 200,
])
->addRule('response', [
->addRule('responseBody', [
'type' => self::TYPE_STRING,
'description' => 'The script response output string. Logs the last 4,000 characters of the execution response output.',
'description' => 'HTTP response body. This will return empty unless execution is created as synchronous.',
'default' => '',
'example' => 'Developers are awesome.',
])
->addRule('responseHeaders', [
'type' => Response::MODEL_HEADERS,
'description' => 'HTTP response headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous.',
'default' => [],
'example' => [['Content-Type' => 'application/json']],
'array' => true,
])
->addRule('logs', [
'type' => self::TYPE_STRING,
'description' => 'Function logs. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'default' => '',
'example' => '',
])
->addRule('stdout', [
->addRule('errors', [
'type' => self::TYPE_STRING,
'description' => 'The script stdout output string. Logs the last 4,000 characters of the execution stdout output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'default' => '',
'example' => '',
])
->addRule('stderr', [
'type' => self::TYPE_STRING,
'description' => 'The script stderr output string. Logs the last 4,000 characters of the execution stderr output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'description' => 'Function errors. Includes the last 4,000 characters. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'default' => '',
'example' => '',
])
->addRule('duration', [
'type' => self::TYPE_FLOAT,
'description' => 'The script execution duration in seconds.',
'description' => 'Function execution duration in seconds.',
'default' => 0,
'example' => 0.400,
])
+61 -13
View File
@@ -49,6 +49,18 @@ class Func extends Model
'default' => true,
'example' => false,
])
->addRule('live', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is the function deployed with the latest configuration? This is set to false if you\'ve changed an environment variables, entrypoint, commands, or other settings that needs redeploy to be applied. When the value is false, redeploy the function to update it with the latest configuration.',
'default' => true,
'example' => false,
])
->addRule('logging', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.',
'default' => true,
'example' => false,
])
->addRule('runtime', [
'type' => self::TYPE_STRING,
'description' => 'Function execution runtime.',
@@ -81,23 +93,59 @@ class Func extends Model
'default' => '',
'example' => '5 4 * * *',
])
->addRule('scheduleNext', [
'type' => self::TYPE_DATETIME,
'description' => 'Function\'s next scheduled execution time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('schedulePrevious', [
'type' => self::TYPE_DATETIME,
'description' => 'Function\'s previous scheduled execution time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('timeout', [
'type' => self::TYPE_INTEGER,
'description' => 'Function execution timeout in seconds.',
'default' => 15,
'example' => 15,
'example' => 300,
])
->addRule('entrypoint', [
'type' => self::TYPE_STRING,
'description' => 'The entrypoint file used to execute the deployment.',
'default' => '',
'example' => 'index.js',
])
->addRule('commands', [
'type' => self::TYPE_STRING,
'description' => 'The build command used to build the deployment.',
'default' => '',
'example' => 'npm install',
])
->addRule('version', [
'type' => self::TYPE_STRING,
'description' => 'Version of Open Runtimes used for the function.',
'default' => 'v3',
'example' => 'v2',
])
->addRule('installationId', [
'type' => self::TYPE_STRING,
'description' => 'Function VCS (Version Control System) installation id.',
'default' => '',
'example' => '6m40at4ejk5h2u9s1hboo',
])
->addRule('providerRepositoryId', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Repository ID',
'default' => '',
'example' => 'appwrite',
])
->addRule('providerBranch', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) branch name',
'default' => '',
'example' => 'main',
])
->addRule('providerRootDirectory', [
'type' => self::TYPE_STRING,
'description' => 'Path to function in VCS (Version Control System) repository',
'default' => '',
'example' => 'functions/helloWorld',
])
->addRule('providerSilentMode', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is VCS (Version Control System) connection is in silent mode? When in silence mode, no comments will be posted on the repository pull or merge requests',
'default' => false,
'example' => false,
])
;
}
@@ -0,0 +1,46 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Headers extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Header name.',
'default' => '',
'example' => 'Content-Type',
])
->addRule('value', [
'type' => self::TYPE_STRING,
'description' => 'Header value.',
'default' => '',
'example' => 'application/json',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Headers';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_HEADERS;
}
}
@@ -10,6 +10,12 @@ class HealthStatus extends Model
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Name of the service.',
'default' => '',
'example' => 'database',
])
->addRule('ping', [
'type' => self::TYPE_INTEGER,
'description' => 'Duration in milliseconds how long the health check took.',
@@ -0,0 +1,95 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Identity extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Identity ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Identity creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Identity update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'User ID.',
'default' => '',
'example' => '5e5bb8c16897e',
])
->addRule('provider', [
'type' => self::TYPE_STRING,
'description' => 'Identity Provider.',
'default' => '',
'example' => 'email',
])
->addRule('providerUid', [
'type' => self::TYPE_STRING,
'description' => 'ID of the User in the Identity Provider.',
'default' => '',
'example' => '5e5bb8c16897e',
])
->addRule('providerEmail', [
'type' => self::TYPE_STRING,
'description' => 'Email of the User in the Identity Provider.',
'default' => '',
'example' => 'user@example.com',
])
->addRule('providerAccessToken', [
'type' => self::TYPE_STRING,
'description' => 'Identity Provider Access Token.',
'default' => '',
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
])
->addRule('providerAccessTokenExpiry', [
'type' => self::TYPE_DATETIME,
'description' => 'The date of when the access token expires in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('providerRefreshToken', [
'type' => self::TYPE_STRING,
'description' => 'Identity Provider Refresh Token.',
'default' => '',
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Identity';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_IDENTITY;
}
}
@@ -28,6 +28,12 @@ class Index extends Model
'default' => '',
'example' => 'available',
])
->addRule('error', [
'type' => self::TYPE_STRING,
'description' => 'Error message. Displays error generated on failure of creating or deleting an index.',
'default' => '',
'example' => 'string',
])
->addRule('attributes', [
'type' => self::TYPE_STRING,
'description' => 'Index attributes.',
@@ -0,0 +1,72 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Installation extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Function ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Function creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Function update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('provider', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) provider name.',
'default' => [],
'example' => 'github',
'array' => false,
])
->addRule('organization', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) organization name.',
'default' => [],
'example' => 'appwrite',
'array' => false,
])
->addRule('providerInstallationId', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) installation ID.',
'default' => '',
'example' => '5322',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Installation';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_INSTALLATION;
}
}
+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.',
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_KEY_ACCCESS / 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 LocaleCode extends Model
{
public function __construct()
{
$this
->addRule('code', [
'type' => self::TYPE_STRING,
'description' => 'Locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)',
'default' => '',
'example' => 'en-us',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Locale name',
'default' => '',
'example' => 'US',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'LocaleCode';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_LOCALE_CODE;
}
}

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