mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.4.x' into add-error-attribute
# Conflicts: # CHANGES.md # app/workers/databases.php # composer.json # composer.lock
This commit is contained in:
@@ -12,7 +12,7 @@ use Appwrite\Auth\Hash\Sha;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
@@ -30,7 +30,7 @@ class Auth
|
||||
];
|
||||
|
||||
public const DEFAULT_ALGO = 'argon2';
|
||||
public const DEFAULT_ALGO_OPTIONS = ['memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
|
||||
public const DEFAULT_ALGO_OPTIONS = ['type' => 'argon2', 'memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
|
||||
|
||||
/**
|
||||
* User Roles.
|
||||
@@ -352,19 +352,19 @@ class Auth
|
||||
*
|
||||
* @param array $sessions
|
||||
* @param string $secret
|
||||
* @param string $expires
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function sessionVerify(array $sessions, string $secret)
|
||||
public static function sessionVerify(array $sessions, string $secret, int $expires)
|
||||
{
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
if (
|
||||
$session->isSet('secret') &&
|
||||
$session->isSet('expire') &&
|
||||
$session->isSet('provider') &&
|
||||
$session->getAttribute('secret') === self::hash($secret) &&
|
||||
DateTime::formatTz($session->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
|
||||
DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $expires)) >= DateTime::formatTz(DateTime::now())
|
||||
) {
|
||||
return $session->getId();
|
||||
}
|
||||
|
||||
+19
-22
@@ -1,20 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
/**
|
||||
* Domain
|
||||
* Password.
|
||||
*
|
||||
* Validate that an variable is a valid domain address
|
||||
*
|
||||
* @package Utopia\Validator
|
||||
* Validates user password string
|
||||
*/
|
||||
class Domain extends Validator
|
||||
class PasswordDictionary extends Password
|
||||
{
|
||||
protected array $dictionary;
|
||||
protected bool $enabled;
|
||||
|
||||
public function __construct(array $dictionary, bool $enabled = false)
|
||||
{
|
||||
$this->dictionary = $dictionary;
|
||||
$this->enabled = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
* Get Description.
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
@@ -22,33 +27,25 @@ class Domain extends Validator
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must be a valid domain';
|
||||
return 'Password must be at least 8 characters and should not be one of the commonly used password.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
* Is valid.
|
||||
*
|
||||
* Validation will pass when $value is valid domain.
|
||||
* @param mixed $value
|
||||
*
|
||||
* Validates domain names against RFC 1034, RFC 1035, RFC 952, RFC 1123, RFC 2732, RFC 2181, and RFC 1123.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (empty($value)) {
|
||||
if (!parent::isValid($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
if ($this->enabled && array_key_exists($value, $this->dictionary)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\filter_var($value, FILTER_VALIDATE_DOMAIN) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
|
||||
/**
|
||||
* Password.
|
||||
*
|
||||
* Validates user password string
|
||||
*/
|
||||
class PasswordHistory extends Password
|
||||
{
|
||||
protected array $history;
|
||||
protected string $algo;
|
||||
protected array $algoOptions;
|
||||
|
||||
public function __construct(array $history, string $algo, array $algoOptions = [])
|
||||
{
|
||||
$this->history = $history;
|
||||
$this->algo = $algo;
|
||||
$this->algoOptions = $algoOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description.
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Password shouldn\'t be in the history.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
foreach ($this->history as $hash) {
|
||||
if (Auth::passwordVerify($value, $hash, $this->algo, $this->algoOptions)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Env
|
||||
$data = explode("\n", $data);
|
||||
|
||||
foreach ($data as &$row) {
|
||||
$row = explode('=', $row);
|
||||
$row = explode('=', $row, 2);
|
||||
$key = (isset($row[0])) ? trim($row[0]) : null;
|
||||
$value = (isset($row[1])) ? trim($row[1]) : null;
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ class Delete extends Event
|
||||
protected ?Document $document = null;
|
||||
protected ?string $resource = null;
|
||||
protected ?string $datetime = null;
|
||||
protected ?string $dateTime30m = null;
|
||||
protected ?string $dateTime1d = null;
|
||||
protected ?string $hourlyUsageRetentionDatetime = null;
|
||||
|
||||
|
||||
public function __construct()
|
||||
@@ -56,26 +55,14 @@ class Delete extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Set datetime for 1 day interval.
|
||||
* Sets datetime for 1h interval.
|
||||
*
|
||||
* @param string $datetime
|
||||
* @return self
|
||||
*/
|
||||
public function setDateTime1d(string $datetime): self
|
||||
public function setUsageRetentionHourlyDateTime(string $datetime): self
|
||||
{
|
||||
$this->dateTime1d = $datetime;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets datetime for 30m interval.
|
||||
*
|
||||
* @param string $datetime
|
||||
* @return self
|
||||
*/
|
||||
public function setDateTime30m(string $datetime): self
|
||||
{
|
||||
$this->dateTime30m = $datetime;
|
||||
$this->hourlyUsageRetentionDatetime = $datetime;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -140,8 +127,7 @@ class Delete extends Event
|
||||
'document' => $this->document,
|
||||
'resource' => $this->resource,
|
||||
'datetime' => $this->datetime,
|
||||
'dateTime1d' => $this->dateTime1d,
|
||||
'dateTime30m' => $this->dateTime30m,
|
||||
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+27
-55
@@ -8,11 +8,10 @@ use Utopia\Database\Document;
|
||||
class Mail extends Event
|
||||
{
|
||||
protected string $recipient = '';
|
||||
protected string $url = '';
|
||||
protected string $type = '';
|
||||
protected string $from = '';
|
||||
protected string $name = '';
|
||||
protected string $locale = '';
|
||||
protected ?Document $team = null;
|
||||
protected string $subject = '';
|
||||
protected string $body = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -20,14 +19,14 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets team for the mail event.
|
||||
* Sets subject for the mail event.
|
||||
*
|
||||
* @param Document $team
|
||||
* @param string $subject
|
||||
* @return self
|
||||
*/
|
||||
public function setTeam(Document $team): self
|
||||
public function setSubject(string $subject): self
|
||||
{
|
||||
$this->team = $team;
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -35,11 +34,11 @@ class Mail extends Event
|
||||
/**
|
||||
* Returns set team for the mail event.
|
||||
*
|
||||
* @return null|Document
|
||||
* @return string
|
||||
*/
|
||||
public function getTeam(): ?Document
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->team;
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,49 +65,49 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets url for the mail event.
|
||||
* Sets from for the mail event.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $from
|
||||
* @return self
|
||||
*/
|
||||
public function setUrl(string $url): self
|
||||
public function setFrom(string $from): self
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->from = $from;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set url for the mail event.
|
||||
* Returns from for mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getURL(): string
|
||||
public function getFrom(): string
|
||||
{
|
||||
return $this->url;
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets type for the mail event (use the constants starting with MAIL_TYPE_*).
|
||||
* Sets body for the mail event.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $body
|
||||
* @return self
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
public function setBody(string $body): self
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set type for the mail event.
|
||||
* Returns body for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->type;
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,29 +133,6 @@ class Mail extends Event
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets locale for the mail event.
|
||||
*
|
||||
* @param string $locale
|
||||
* @return self
|
||||
*/
|
||||
public function setLocale(string $locale): self
|
||||
{
|
||||
$this->locale = $locale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set locale for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the event and sends it to the mails worker.
|
||||
*
|
||||
@@ -166,15 +142,11 @@ class Mail extends Event
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
return Resque::enqueue($this->queue, $this->class, [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'payload' => $this->payload,
|
||||
'from' => $this->from,
|
||||
'recipient' => $this->recipient,
|
||||
'url' => $this->url,
|
||||
'locale' => $this->locale,
|
||||
'type' => $this->type,
|
||||
'name' => $this->name,
|
||||
'team' => $this->team,
|
||||
'subject' => $this->subject,
|
||||
'body' => $this->body,
|
||||
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class Exception extends \Exception
|
||||
* - Keys
|
||||
* - Platform
|
||||
* - Domain
|
||||
* - GraphQL
|
||||
*/
|
||||
|
||||
/** General */
|
||||
@@ -50,6 +51,7 @@ 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_USAGE_DISABLED = 'general_usage_disabled';
|
||||
|
||||
/** Users */
|
||||
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
|
||||
@@ -64,6 +66,7 @@ class Exception extends \Exception
|
||||
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_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';
|
||||
@@ -133,6 +136,8 @@ class Exception extends \Exception
|
||||
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
|
||||
public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
|
||||
public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
|
||||
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
|
||||
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
|
||||
|
||||
/** Attribute */
|
||||
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
|
||||
@@ -143,6 +148,7 @@ class Exception extends \Exception
|
||||
public const ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists';
|
||||
public const ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded';
|
||||
public const ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid';
|
||||
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
|
||||
|
||||
/** Indexes */
|
||||
public const INDEX_NOT_FOUND = 'index_not_found';
|
||||
@@ -154,6 +160,7 @@ class Exception extends \Exception
|
||||
public const PROJECT_UNKNOWN = 'project_unknown';
|
||||
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
|
||||
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||
public const PROJECT_INVALID_SUCCESS_URL = 'project_invalid_success_url';
|
||||
public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
||||
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
||||
@@ -176,6 +183,11 @@ class Exception extends \Exception
|
||||
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';
|
||||
|
||||
protected $type = '';
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use GraphQL\Error\ClientAware;
|
||||
|
||||
class Exception extends AppwriteException implements ClientAware
|
||||
{
|
||||
public function isClientSafe(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'appwrite';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Promises;
|
||||
|
||||
use Appwrite\Promises\Promise;
|
||||
use GraphQL\Executor\Promise\Promise as GQLPromise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
|
||||
abstract class Adapter implements PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* Returns true if the given value is a {@see Promise}.
|
||||
*
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isThenable($value): bool
|
||||
{
|
||||
return $value instanceof Promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@see Promise} into a {@see GQLPromise}
|
||||
*
|
||||
* @param mixed $thenable
|
||||
* @return GQLPromise
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function convertThenable(mixed $thenable): GQLPromise
|
||||
{
|
||||
if (!$thenable instanceof Promise) {
|
||||
throw new \Exception('Expected instance of Promise got: ' . \gettype($thenable));
|
||||
}
|
||||
|
||||
return new GQLPromise($thenable, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the passed in promise resolves.
|
||||
*
|
||||
* @param GQLPromise $promise
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
* @return GQLPromise
|
||||
*/
|
||||
public function then(
|
||||
GQLPromise $promise,
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
): GQLPromise {
|
||||
/** @var Promise $adoptedPromise */
|
||||
$adoptedPromise = $promise->adoptedPromise;
|
||||
|
||||
return new GQLPromise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise with the given resolver function.
|
||||
*
|
||||
* @param callable $resolver
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function create(callable $resolver): GQLPromise;
|
||||
|
||||
/**
|
||||
* Create a new promise that is fulfilled with the given value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function createFulfilled(mixed $value = null): GQLPromise;
|
||||
|
||||
/**
|
||||
* Create a new promise that is rejected with the given reason.
|
||||
*
|
||||
* @param mixed $reason
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function createRejected(mixed $reason): GQLPromise;
|
||||
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function all(array $promisesOrValues): GQLPromise;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Promises\Adapter;
|
||||
|
||||
use Appwrite\GraphQL\Promises\Adapter;
|
||||
use Appwrite\Promises\Swoole as SwoolePromise;
|
||||
use GraphQL\Executor\Promise\Promise as GQLPromise;
|
||||
|
||||
class Swoole extends Adapter
|
||||
{
|
||||
public function create(callable $resolver): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($resolver) {
|
||||
$resolver($resolve, $reject);
|
||||
});
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function createFulfilled($value = null): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function createRejected($reason): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($reason) {
|
||||
$reject($reason);
|
||||
});
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
{
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Exception as GQLException;
|
||||
use Appwrite\Promises\Swoole;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Exception;
|
||||
use Utopia\Route;
|
||||
|
||||
class Resolvers
|
||||
{
|
||||
/**
|
||||
* Create a resolver for a given API {@see Route}.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param ?Route $route
|
||||
* @return callable
|
||||
*/
|
||||
public static function api(
|
||||
App $utopia,
|
||||
?Route $route,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
/** @var App $utopia */
|
||||
/** @var Response $response */
|
||||
/** @var Request $request */
|
||||
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$path = $route->getPath();
|
||||
foreach ($args as $key => $value) {
|
||||
if (\str_contains($path, '/:' . $key)) {
|
||||
$path = \str_replace(':' . $key, $value, $path);
|
||||
}
|
||||
}
|
||||
|
||||
$request->setMethod($route->getMethod());
|
||||
$request->setURI($path);
|
||||
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$request->setQueryString($args);
|
||||
break;
|
||||
default:
|
||||
$request->setPayload($args);
|
||||
break;
|
||||
}
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for a document in a specified database and collection with a specific method type.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param string $methodType
|
||||
* @return callable
|
||||
*/
|
||||
public static function document(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
string $methodType,
|
||||
): callable {
|
||||
return [self::class, 'document' . \ucfirst($methodType)](
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for getting a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentGet(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for listing documents in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @param callable $params
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentList(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setQueryString($params($databaseId, $collectionId, $args));
|
||||
|
||||
$beforeResolve = function ($payload) {
|
||||
return $payload['documents'];
|
||||
};
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for creating a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @param callable $params
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentCreate(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('POST');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for updating a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @param callable $params
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentUpdate(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('PATCH');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for deleting a document in a specified database and collection.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param string $databaseId
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentDelete(
|
||||
App $utopia,
|
||||
string $databaseId,
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('DELETE');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param App $utopia
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @param callable|null $beforeResolve
|
||||
* @param callable|null $beforeReject
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function resolve(
|
||||
App $utopia,
|
||||
Request $request,
|
||||
Response $response,
|
||||
callable $resolve,
|
||||
callable $reject,
|
||||
?callable $beforeResolve = null,
|
||||
?callable $beforeReject = null,
|
||||
): void {
|
||||
// Drop json content type so post args are used directly
|
||||
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
|
||||
$request->removeHeader('content-type');
|
||||
}
|
||||
|
||||
$request = clone $request;
|
||||
$utopia->setResource('request', static fn() => $request);
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
|
||||
try {
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
|
||||
$utopia->execute($route, $request);
|
||||
} catch (\Throwable $e) {
|
||||
if ($beforeReject) {
|
||||
$e = $beforeReject($e);
|
||||
}
|
||||
$reject($e);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $response->getPayload();
|
||||
|
||||
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
|
||||
if ($beforeReject) {
|
||||
$payload = $beforeReject($payload);
|
||||
}
|
||||
$reject(new GQLException(
|
||||
message: $payload['message'],
|
||||
code: $response->getStatusCode()
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = self::escapePayload($payload, 1);
|
||||
|
||||
if ($beforeResolve) {
|
||||
$payload = $beforeResolve($payload);
|
||||
}
|
||||
|
||||
$resolve($payload);
|
||||
}
|
||||
|
||||
private static function escapePayload(array $payload, int $depth)
|
||||
{
|
||||
if ($depth > App::getEnv('_APP_GRAPHQL_MAX_DEPTH', 3)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($payload as $key => $value) {
|
||||
if (\str_starts_with($key, '$')) {
|
||||
$escapedKey = \str_replace('$', '_', $key);
|
||||
$payload[$escapedKey] = $value;
|
||||
unset($payload[$key]);
|
||||
}
|
||||
|
||||
if (\is_array($value)) {
|
||||
$payload[$key] = self::escapePayload($value, $depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\Mapper;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema as GQLSchema;
|
||||
use Utopia\App;
|
||||
use Utopia\Exception;
|
||||
use Utopia\Route;
|
||||
|
||||
class Schema
|
||||
{
|
||||
protected static ?GQLSchema $schema = null;
|
||||
protected static array $dirty = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param callable $complexity Function to calculate complexity
|
||||
* @param callable $attributes Function to get attributes
|
||||
* @param array $urls Array of functions to get urls for specific method types
|
||||
* @param array $params Array of functions to build parameters for specific method types
|
||||
* @return GQLSchema
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function build(
|
||||
App $utopia,
|
||||
callable $complexity,
|
||||
callable $attributes,
|
||||
array $urls,
|
||||
array $params,
|
||||
): GQLSchema {
|
||||
App::setResource('utopia:graphql', static function () use ($utopia) {
|
||||
return $utopia;
|
||||
});
|
||||
|
||||
if (!empty(self::$schema)) {
|
||||
return self::$schema;
|
||||
}
|
||||
|
||||
$api = static::api(
|
||||
$utopia,
|
||||
$complexity
|
||||
);
|
||||
//$collections = static::collections(
|
||||
// $utopia,
|
||||
// $complexity,
|
||||
// $attributes,
|
||||
// $urls,
|
||||
// $params,
|
||||
//);
|
||||
|
||||
$queries = \array_merge_recursive(
|
||||
$api['query'],
|
||||
//$collections['query']
|
||||
);
|
||||
$mutations = \array_merge_recursive(
|
||||
$api['mutation'],
|
||||
//$collections['mutation']
|
||||
);
|
||||
|
||||
\ksort($queries);
|
||||
\ksort($mutations);
|
||||
|
||||
return static::$schema = new GQLSchema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => $queries
|
||||
]),
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => $mutations
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function iterates all API routes and builds a GraphQL
|
||||
* schema defining types and resolvers for all response models.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param callable $complexity
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
protected static function api(App $utopia, callable $complexity): array
|
||||
{
|
||||
Mapper::init($utopia
|
||||
->getResource('response')
|
||||
->getModels());
|
||||
|
||||
$queries = [];
|
||||
$mutations = [];
|
||||
|
||||
foreach ($utopia->getRoutes() as $routes) {
|
||||
foreach ($routes as $route) {
|
||||
/** @var Route $route */
|
||||
|
||||
$namespace = $route->getLabel('sdk.namespace', '');
|
||||
$method = $route->getLabel('sdk.method', '');
|
||||
$name = $namespace . \ucfirst($method);
|
||||
|
||||
if (empty($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$queries[$name] = $field;
|
||||
break;
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$mutations[$name] = $field;
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unsupported method: {$route->getMethod()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $queries,
|
||||
'mutation' => $mutations
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates all of a projects attributes and builds GraphQL
|
||||
* queries and mutations for the collections they make up.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param callable $complexity
|
||||
* @param callable $attributes
|
||||
* @param array $urls
|
||||
* @param array $params
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function collections(
|
||||
App $utopia,
|
||||
callable $complexity,
|
||||
callable $attributes,
|
||||
array $urls,
|
||||
array $params,
|
||||
): array {
|
||||
$collections = [];
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$limit = 1000;
|
||||
$offset = 0;
|
||||
|
||||
while (!empty($attrs = $attributes($limit, $offset))) {
|
||||
foreach ($attrs as $attr) {
|
||||
if ($attr['status'] !== 'available') {
|
||||
continue;
|
||||
}
|
||||
$databaseId = $attr['databaseId'];
|
||||
$collectionId = $attr['collectionId'];
|
||||
$key = $attr['key'];
|
||||
$type = $attr['type'];
|
||||
$array = $attr['array'];
|
||||
$required = $attr['required'];
|
||||
$default = $attr['default'];
|
||||
$escapedKey = str_replace('$', '', $key);
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
'type' => Mapper::attribute(
|
||||
$type,
|
||||
$array,
|
||||
$required
|
||||
),
|
||||
'defaultValue' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
Mapper::args('mutate')
|
||||
);
|
||||
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentGet(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['get'],
|
||||
)
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => Type::listOf($objectType),
|
||||
'args' => Mapper::args('list'),
|
||||
'resolve' => Resolvers::documentList(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['list'],
|
||||
$params['list'],
|
||||
),
|
||||
'complexity' => $complexity,
|
||||
];
|
||||
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::documentCreate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['create'],
|
||||
$params['create'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => \array_merge(
|
||||
Mapper::args('id'),
|
||||
\array_map(
|
||||
fn($attr) => $attr['type'] = Type::getNullableType($attr['type']),
|
||||
$attributes
|
||||
)
|
||||
),
|
||||
'resolve' => Resolvers::documentUpdate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['update'],
|
||||
$params['update'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => Mapper::model('none'),
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentDelete(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['delete'],
|
||||
)
|
||||
];
|
||||
}
|
||||
$offset += $limit;
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $queryFields,
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
public static function setDirty(string $projectId): void
|
||||
{
|
||||
self::$dirty[$projectId] = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\Assoc;
|
||||
use Appwrite\GraphQL\Types\InputFile;
|
||||
use Appwrite\GraphQL\Types\Json;
|
||||
use Appwrite\GraphQL\Types\Registry;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class Types
|
||||
{
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function json(): Type
|
||||
{
|
||||
if (Registry::has(Json::class)) {
|
||||
return Registry::get(Json::class);
|
||||
}
|
||||
$type = new Json();
|
||||
Registry::set(Json::class, $type);
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function assoc(): Type
|
||||
{
|
||||
if (Registry::has(Assoc::class)) {
|
||||
return Registry::get(Assoc::class);
|
||||
}
|
||||
$type = new Assoc();
|
||||
Registry::set(Assoc::class, $type);
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the InputFile type.
|
||||
*
|
||||
* @return InputFile
|
||||
*/
|
||||
public static function inputFile(): Type
|
||||
{
|
||||
if (Registry::has(InputFile::class)) {
|
||||
return Registry::get(InputFile::class);
|
||||
}
|
||||
$type = new InputFile();
|
||||
Registry::set(InputFile::class, $type);
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectValueNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return \json_encode($value);
|
||||
}
|
||||
|
||||
public function parseValue($value)
|
||||
{
|
||||
if (\is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return \json_decode($value, true);
|
||||
}
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
return \json_decode($valueNode->value, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
public $name = 'InputFile';
|
||||
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function parseValue($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
throw new Error('`InputFile` cannot be hardcoded in query, be sure to conform to GraphQL multipart request specification. Instead got: ' . $valueNode->kind, $valueNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectValueNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Json extends ScalarType
|
||||
{
|
||||
public $name = 'Json';
|
||||
public $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function parseValue($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
switch ($valueNode) {
|
||||
case $valueNode instanceof StringValueNode:
|
||||
case $valueNode instanceof BooleanValueNode:
|
||||
return $valueNode->value;
|
||||
case $valueNode instanceof IntValueNode:
|
||||
case $valueNode instanceof FloatValueNode:
|
||||
return floatval($valueNode->value);
|
||||
case $valueNode instanceof ObjectValueNode:
|
||||
$value = [];
|
||||
foreach ($valueNode->fields as $field) {
|
||||
$value[$field->name->value] =
|
||||
$this->parseLiteral($field->value);
|
||||
}
|
||||
return $value;
|
||||
case ($valueNode instanceof ListValueNode):
|
||||
return array_map([$this, 'parseLiteral'], $valueNode->values);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use Appwrite\GraphQL\Resolvers;
|
||||
use Appwrite\GraphQL\Types;
|
||||
use Exception;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use Utopia\App;
|
||||
use Utopia\Route;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class Mapper
|
||||
{
|
||||
private static array $models = [];
|
||||
private static array $args = [];
|
||||
private static array $blacklist = [
|
||||
'/v1/mock',
|
||||
'/v1/graphql',
|
||||
'/v1/account/sessions/oauth2',
|
||||
];
|
||||
|
||||
public static function init(array $models): void
|
||||
{
|
||||
self::$models = $models;
|
||||
|
||||
self::$args = [
|
||||
'id' => [
|
||||
'id' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
],
|
||||
],
|
||||
'list' => [
|
||||
'queries' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::string())),
|
||||
'defaultValue' => [],
|
||||
],
|
||||
],
|
||||
'mutate' => [
|
||||
'permissions' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::string())),
|
||||
'defaultValue' => [],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$defaults = [
|
||||
'boolean' => Type::boolean(),
|
||||
'string' => Type::string(),
|
||||
'integer' => Type::int(),
|
||||
'double' => Type::float(),
|
||||
'datetime' => Type::string(),
|
||||
'json' => Types::json(),
|
||||
'none' => Types::json(),
|
||||
'any' => Types::json(),
|
||||
];
|
||||
|
||||
foreach ($defaults as $type => $default) {
|
||||
Registry::set($type, $default);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered default arguments for a given key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public static function args(string $key): array
|
||||
{
|
||||
return self::$args[$key] ?? [];
|
||||
}
|
||||
|
||||
public static function route(
|
||||
App $utopia,
|
||||
Route $route,
|
||||
callable $complexity
|
||||
): iterable {
|
||||
foreach (self::$blacklist as $blacklist) {
|
||||
if (\str_starts_with($route->getPath(), $blacklist)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$names = $route->getLabel('sdk.response.model', 'none');
|
||||
$models = \is_array($names)
|
||||
? \array_map(static fn($m) => static::$models[$m], $names)
|
||||
: [static::$models[$names]];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$type = Mapper::model(\ucfirst($model->getType()));
|
||||
$description = $route->getDesc();
|
||||
$params = [];
|
||||
$list = false;
|
||||
|
||||
foreach ($route->getParams() as $name => $parameter) {
|
||||
if ($name === 'queries') {
|
||||
$list = true;
|
||||
}
|
||||
$parameterType = Mapper::param(
|
||||
$utopia,
|
||||
$parameter['validator'],
|
||||
!$parameter['optional'],
|
||||
$parameter['injections']
|
||||
);
|
||||
$params[$name] = [
|
||||
'type' => $parameterType,
|
||||
'description' => $parameter['description'],
|
||||
];
|
||||
}
|
||||
|
||||
$field = [
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'args' => $params,
|
||||
'resolve' => Resolvers::api($utopia, $route)
|
||||
];
|
||||
|
||||
if ($list) {
|
||||
$field['complexity'] = $complexity;
|
||||
}
|
||||
|
||||
yield $field;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type from the registry, creating it if it does not already exist.
|
||||
*
|
||||
* @param string $name
|
||||
* @return Type
|
||||
*/
|
||||
public static function model(string $name): Type
|
||||
{
|
||||
if (Registry::has($name)) {
|
||||
return Registry::get($name);
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$model = self::$models[\lcfirst($name)];
|
||||
|
||||
// If model has additional properties, explicitly add a 'data' field
|
||||
if ($model->isAny()) {
|
||||
$fields['data'] = [
|
||||
'type' => Type::string(),
|
||||
'description' => 'Additional data',
|
||||
'resolve' => static function ($object, $args, $context, $info) {
|
||||
$data = \array_filter(
|
||||
(array)$object,
|
||||
fn($key) => !\str_starts_with($key, '_'),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
return \json_encode($data, JSON_FORCE_OBJECT);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// If model has no properties, explicitly add a 'status' field
|
||||
// because GraphQL requires at least 1 field per type.
|
||||
if (!$model->isAny() && empty($model->getRules())) {
|
||||
$fields['status'] = [
|
||||
'type' => Type::string(),
|
||||
'description' => 'Status',
|
||||
'resolve' => static fn($object, $args, $context, $info) => 'OK',
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($model->getRules() as $key => $rule) {
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$type = self::getUnionType($escapedKey, $rule);
|
||||
} else {
|
||||
$type = self::getObjectType($rule);
|
||||
}
|
||||
|
||||
if ($rule['array']) {
|
||||
$type = Type::listOf($type);
|
||||
}
|
||||
|
||||
$fields[$escapedKey] = [
|
||||
'type' => $type,
|
||||
'description' => $rule['description'],
|
||||
];
|
||||
|
||||
if (!$rule['required']) {
|
||||
$fields[$escapedKey]['defaultValue'] = $rule['default'];
|
||||
}
|
||||
}
|
||||
|
||||
$type = new ObjectType([
|
||||
'name' => $name,
|
||||
'fields' => $fields,
|
||||
]);
|
||||
|
||||
Registry::set($name, $type);
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a {@see Route} parameter to a GraphQL Type
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Validator|callable $validator
|
||||
* @param bool $required
|
||||
* @param array $injections
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function param(
|
||||
App $utopia,
|
||||
Validator|callable $validator,
|
||||
bool $required,
|
||||
array $injections
|
||||
): Type {
|
||||
$validator = \is_callable($validator)
|
||||
? \call_user_func_array($validator, $utopia->getResources($injections))
|
||||
: $validator;
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
if ($isNullable) {
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
switch ((!empty($validator)) ? $validator::class : '') {
|
||||
case 'Appwrite\Network\Validator\CNAME':
|
||||
case 'Appwrite\Task\Validator\Cron':
|
||||
case 'Appwrite\Utopia\Database\Validator\CustomId':
|
||||
case 'Utopia\Validator\Domain':
|
||||
case 'Appwrite\Network\Validator\Email':
|
||||
case 'Appwrite\Event\Validator\Event':
|
||||
case 'Utopia\Validator\HexColor':
|
||||
case 'Utopia\Validator\Host':
|
||||
case 'Utopia\Validator\IP':
|
||||
case 'Utopia\Database\Validator\Key':
|
||||
case 'Utopia\Validator\Origin':
|
||||
case 'Appwrite\Auth\Validator\Password':
|
||||
case 'Utopia\Validator\Text':
|
||||
case 'Utopia\Database\Validator\UID':
|
||||
case 'Utopia\Validator\URL':
|
||||
case 'Utopia\Validator\WhiteList':
|
||||
default:
|
||||
$type = Type::string();
|
||||
break;
|
||||
case 'Utopia\Database\Validator\Authorization':
|
||||
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\Databases':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
|
||||
case 'Appwrite\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\Memberships':
|
||||
case 'Utopia\Database\Validator\Permissions':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries':
|
||||
case 'Utopia\Database\Validator\Roles':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
|
||||
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
|
||||
$type = Type::listOf(Type::string());
|
||||
break;
|
||||
case 'Utopia\Validator\Boolean':
|
||||
$type = Type::boolean();
|
||||
break;
|
||||
case 'Utopia\Validator\ArrayList':
|
||||
$type = Type::listOf(self::param(
|
||||
$utopia,
|
||||
$validator->getValidator(),
|
||||
$required,
|
||||
$injections
|
||||
));
|
||||
break;
|
||||
case 'Utopia\Validator\Integer':
|
||||
case 'Utopia\Validator\Numeric':
|
||||
case 'Utopia\Validator\Range':
|
||||
$type = Type::int();
|
||||
break;
|
||||
case 'Utopia\Validator\FloatValidator':
|
||||
$type = Type::float();
|
||||
break;
|
||||
case 'Utopia\Validator\Assoc':
|
||||
$type = Types::assoc();
|
||||
break;
|
||||
case 'Utopia\Validator\JSON':
|
||||
$type = Types::json();
|
||||
break;
|
||||
case 'Utopia\Storage\Validator\File':
|
||||
$type = Types::inputFile();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($required && !$isNullable) {
|
||||
$type = Type::nonNull($type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an {@see Attribute} to a GraphQL Type
|
||||
*
|
||||
* @param string $type
|
||||
* @param bool $array
|
||||
* @param bool $required
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function attribute(string $type, bool $array, bool $required): Type
|
||||
{
|
||||
if ($array) {
|
||||
return Type::listOf(self::attribute(
|
||||
$type,
|
||||
false,
|
||||
$required
|
||||
));
|
||||
}
|
||||
|
||||
$type = match ($type) {
|
||||
'boolean' => Type::boolean(),
|
||||
'integer' => Type::int(),
|
||||
'double' => Type::float(),
|
||||
default => Type::string(),
|
||||
};
|
||||
|
||||
if ($required) {
|
||||
$type = Type::nonNull($type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
private static function getObjectType(array $rule): Type
|
||||
{
|
||||
$type = $rule['type'];
|
||||
|
||||
if (Registry::has($type)) {
|
||||
return Registry::get($type);
|
||||
}
|
||||
|
||||
$complexModel = self::$models[$type];
|
||||
return self::model(\ucfirst($complexModel->getType()));
|
||||
}
|
||||
|
||||
private static function getUnionType(string $name, array $rule): Type
|
||||
{
|
||||
$unionName = \ucfirst($name);
|
||||
|
||||
if (Registry::has($unionName)) {
|
||||
return Registry::get($unionName);
|
||||
}
|
||||
|
||||
$types = [];
|
||||
foreach ($rule['type'] as $type) {
|
||||
$types[] = self::model(\ucfirst($type));
|
||||
}
|
||||
|
||||
$unionType = new UnionType([
|
||||
'name' => $unionName,
|
||||
'types' => $types,
|
||||
'resolveType' => static function ($object) use ($unionName) {
|
||||
return static::getUnionImplementation($unionName, $object);
|
||||
},
|
||||
]);
|
||||
|
||||
Registry::set($unionName, $unionType);
|
||||
|
||||
return $unionType;
|
||||
}
|
||||
|
||||
private static function getUnionImplementation(string $name, array $object): Type
|
||||
{
|
||||
// TODO: Find a better way to do this
|
||||
|
||||
switch ($name) {
|
||||
case 'Attributes':
|
||||
return static::getAttributeImplementation($object);
|
||||
case 'HashOptions':
|
||||
return static::getHashOptionsImplementation($object);
|
||||
}
|
||||
|
||||
throw new Exception('Unknown union type: ' . $name);
|
||||
}
|
||||
|
||||
private static function getAttributeImplementation(array $object): Type
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'string':
|
||||
return match ($object['format'] ?? '') {
|
||||
'email' => static::model('AttributeEmail'),
|
||||
'url' => static::model('AttributeUrl'),
|
||||
'ip' => static::model('AttributeIp'),
|
||||
default => static::model('AttributeString'),
|
||||
};
|
||||
case 'integer':
|
||||
return static::model('AttributeInteger');
|
||||
case 'double':
|
||||
return static::model('AttributeFloat');
|
||||
case 'boolean':
|
||||
return static::model('AttributeBoolean');
|
||||
case 'datetime':
|
||||
return static::model('AttributeDatetime');
|
||||
case 'relationship':
|
||||
return static::model('AttributeRelationship');
|
||||
}
|
||||
|
||||
throw new Exception('Unknown attribute implementation');
|
||||
}
|
||||
|
||||
private static function getHashOptionsImplementation(array $object): Type
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'argon2':
|
||||
return static::model('AlgoArgon2');
|
||||
case 'bcrypt':
|
||||
return static::model('AlgoBcrypt');
|
||||
case 'md5':
|
||||
return static::model('AlgoMd5');
|
||||
case 'phpass':
|
||||
return static::model('AlgoPhpass');
|
||||
case 'scrypt':
|
||||
return static::model('AlgoScrypt');
|
||||
case 'scryptMod':
|
||||
return static::model('AlgoScryptModified');
|
||||
case 'sha':
|
||||
return static::model('AlgoSha');
|
||||
}
|
||||
|
||||
throw new Exception('Unknown hash options implementation');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class Registry
|
||||
{
|
||||
private static array $register = [];
|
||||
|
||||
/**
|
||||
* Check if a type exists in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function has(string $type): bool
|
||||
{
|
||||
return isset(self::$register[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type from the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @return Type
|
||||
*/
|
||||
public static function get(string $type): Type
|
||||
{
|
||||
return self::$register[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a type in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @param Type $typeObject
|
||||
*/
|
||||
public static function set(string $type, Type $typeObject): void
|
||||
{
|
||||
self::$register[$type] = $typeObject;
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Appwrite\Messaging\Adapter;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\ID;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class Realtime extends Adapter
|
||||
{
|
||||
@@ -149,7 +149,7 @@ class Realtime extends Adapter
|
||||
'data' => [
|
||||
'events' => $events,
|
||||
'channels' => $channels,
|
||||
'timestamp' => DateTime::now(),
|
||||
'timestamp' => DateTime::formatTz(DateTime::now()),
|
||||
'payload' => $payload
|
||||
]
|
||||
]));
|
||||
@@ -321,7 +321,7 @@ class Realtime extends Adapter
|
||||
}
|
||||
} elseif ($parts[2] === 'deployments') {
|
||||
$channels[] = 'console';
|
||||
|
||||
$projectId = 'console';
|
||||
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\ID;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
@@ -37,6 +37,11 @@ abstract class Migration
|
||||
*/
|
||||
protected Database $consoleDB;
|
||||
|
||||
/**
|
||||
* @var \PDO
|
||||
*/
|
||||
protected \PDO $pdo;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -44,7 +49,20 @@ abstract class Migration
|
||||
'1.0.0-RC1' => 'V15',
|
||||
'1.0.0' => 'V15',
|
||||
'1.0.1' => 'V15',
|
||||
'1.0.3' => 'V15'
|
||||
'1.0.3' => 'V15',
|
||||
'1.1.0' => 'V16',
|
||||
'1.1.1' => 'V16',
|
||||
'1.1.2' => 'V16',
|
||||
'1.2.0' => 'V17',
|
||||
'1.2.1' => 'V17',
|
||||
'1.3.0' => 'V18',
|
||||
'1.3.1' => 'V18',
|
||||
'1.3.2' => 'V18',
|
||||
'1.3.3' => 'V18',
|
||||
'1.3.4' => 'V18',
|
||||
'1.3.5' => 'V18',
|
||||
'1.3.6' => 'V18',
|
||||
'1.3.7' => 'V18',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -93,6 +111,19 @@ abstract class Migration
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set PDO for Migration.
|
||||
*
|
||||
* @param \PDO $pdo
|
||||
* @return \Appwrite\Migration\Migration
|
||||
*/
|
||||
public function setPDO(\PDO $pdo): self
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through every document.
|
||||
*
|
||||
@@ -326,6 +357,25 @@ abstract class Migration
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a collection attribute's internal type
|
||||
*
|
||||
* @param string $collection
|
||||
* @param string $attribute
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
protected function changeAttributeInternalType(string $collection, string $attribute, string $type): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare("ALTER TABLE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_{$collection}` MODIFY `$attribute` $type;");
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (\Exception $e) {
|
||||
Console::warning($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes migration for set project.
|
||||
*/
|
||||
|
||||
@@ -11,17 +11,12 @@ use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\ID;
|
||||
use Utopia\Database\Permission;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class V15 extends Migration
|
||||
{
|
||||
/**
|
||||
* @var \PDO $pdo
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class V16 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() . ')');
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Collections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function migrateCollections(): void
|
||||
{
|
||||
foreach ($this->collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
switch ($id) {
|
||||
case 'sessions':
|
||||
try {
|
||||
/**
|
||||
* Create 'expire' attribute
|
||||
*/
|
||||
$this->projectDB->deleteAttribute($id, 'expire');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'expire' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'projects':
|
||||
try {
|
||||
/**
|
||||
* Create 'region' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'region');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'region' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Create '_key_team' index
|
||||
*/
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_team');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_team' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
try {
|
||||
/**
|
||||
* Create 'region' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'region');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'region' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(50000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param \Utopia\Database\Document $document
|
||||
* @return \Utopia\Database\Document
|
||||
*/
|
||||
protected function fixDocument(Document $document)
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'projects':
|
||||
/**
|
||||
* Bump version number.
|
||||
*/
|
||||
$document->setAttribute('version', '1.1.0');
|
||||
|
||||
/**
|
||||
* Set default authDuration
|
||||
*/
|
||||
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG
|
||||
]));
|
||||
|
||||
/**
|
||||
* Enable OAuth providers with data
|
||||
*/
|
||||
$authProviders = $document->getAttribute('authProviders', []);
|
||||
|
||||
foreach (Config::getParam('providers') as $provider => $value) {
|
||||
if (!$value['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($authProviders[$provider . 'Appid'] ?? false) && ($authProviders[$provider . 'Secret'] ?? false)) {
|
||||
if (array_key_exists($provider . 'Enabled', $authProviders)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$authProviders[$provider . 'Enabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$document->setAttribute('authProviders', $authProviders);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class V17 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() . ')');
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
Console::info('Migrating Buckets');
|
||||
$this->migrateBuckets();
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrating all Bucket tables.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws \PDOException
|
||||
*/
|
||||
protected function migrateBuckets(): void
|
||||
{
|
||||
foreach ($this->documentsIterator('buckets') as $bucket) {
|
||||
$id = "bucket_{$bucket->getInternalId()}";
|
||||
|
||||
try {
|
||||
$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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Collections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function migrateCollections(): void
|
||||
{
|
||||
foreach ($this->collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
switch ($id) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(50000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param \Utopia\Database\Document $document
|
||||
* @return \Utopia\Database\Document
|
||||
*/
|
||||
protected function fixDocument(Document $document)
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'projects':
|
||||
/**
|
||||
* Bump version number.
|
||||
*/
|
||||
$document->setAttribute('version', '1.2.0');
|
||||
|
||||
/**
|
||||
* Set default maxSessions
|
||||
*/
|
||||
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT
|
||||
]));
|
||||
break;
|
||||
case 'users':
|
||||
/**
|
||||
* Set hashOptions type
|
||||
*/
|
||||
$document->setAttribute('hashOptions', array_merge($document->getAttribute('hashOptions', []), [
|
||||
'type' => $document->getAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
]));
|
||||
break;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class V18 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()}");
|
||||
$this->addDocumentSecurityToProject();
|
||||
|
||||
Console::info('Migrating Databases');
|
||||
$this->migrateDatabases();
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Databases.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function migrateDatabases(): void
|
||||
{
|
||||
foreach ($this->documentsIterator('databases') as $database) {
|
||||
$databaseTable = "database_{$database->getInternalId()}";
|
||||
|
||||
Console::info("Migrating Collections of {$database->getId()} ({$database->getAttribute('name')})");
|
||||
|
||||
foreach ($this->documentsIterator($databaseTable) as $collection) {
|
||||
$collectionTable = "{$databaseTable}_collection_{$collection->getInternalId()}";
|
||||
|
||||
foreach ($collection['attributes'] ?? [] as $attribute) {
|
||||
if ($attribute['type'] !== Database::VAR_FLOAT) {
|
||||
continue;
|
||||
}
|
||||
$this->changeAttributeInternalType($collectionTable, $attribute['key'], 'DOUBLE');
|
||||
}
|
||||
|
||||
try {
|
||||
$documentSecurity = $collection->getAttribute('documentSecurity', false);
|
||||
$permissions = $collection->getPermissions();
|
||||
|
||||
$this->projectDB->updateCollection($collectionTable, $permissions, $documentSecurity);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Collections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
foreach ($this->collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
foreach ($collection['attributes'] ?? [] as $attribute) {
|
||||
if ($attribute['type'] !== Database::VAR_FLOAT) {
|
||||
continue;
|
||||
}
|
||||
$this->changeAttributeInternalType($id, $attribute['$id'], 'DOUBLE');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->projectDB->updateCollection($id, [Permission::create(Role::any())], true);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
switch ($id) {
|
||||
case 'users':
|
||||
try {
|
||||
/**
|
||||
* Create 'passwordHistory' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'passwordHistory');
|
||||
$this->projectDB->deleteCachedCollection($id);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'passwordHistory' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
case 'teams':
|
||||
try {
|
||||
/**
|
||||
* Create 'prefs' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'prefs');
|
||||
$this->projectDB->deleteCachedCollection($id);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'prefs' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
case 'attributes':
|
||||
try {
|
||||
/**
|
||||
* Create 'options' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'options');
|
||||
$this->projectDB->deleteCachedCollection($id);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'options' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
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 'projects':
|
||||
/**
|
||||
* Bump version number.
|
||||
*/
|
||||
$document->setAttribute('version', '1.3.0');
|
||||
|
||||
/**
|
||||
* Set default passwordHistory
|
||||
*/
|
||||
$document->setAttribute('auths', array_merge([
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
], $document->getAttribute('auths', [])));
|
||||
break;
|
||||
case 'users':
|
||||
/**
|
||||
* Default Password history
|
||||
*/
|
||||
$document->setAttribute('passwordHistory', $document->getAttribute('passwordHistory', []));
|
||||
break;
|
||||
case 'teams':
|
||||
/**
|
||||
* Default prefs
|
||||
*/
|
||||
$document->setAttribute('prefs', $document->getAttribute('prefs', new \stdClass()));
|
||||
break;
|
||||
case 'attributes':
|
||||
/**
|
||||
* Default options
|
||||
*/
|
||||
$document->setAttribute('options', $document->getAttribute('options', new \stdClass()));
|
||||
break;
|
||||
case 'buckets':
|
||||
/**
|
||||
* Set the bucket permission in the metadata table
|
||||
*/
|
||||
try {
|
||||
$internalBucketId = "bucket_{$this->project->getInternalId()}";
|
||||
$permissions = $document->getPermissions();
|
||||
$fileSecurity = $document->getAttribute('fileSecurity', false);
|
||||
$this->projectDB->updateCollection($internalBucketId, $permissions, $fileSecurity);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
protected function addDocumentSecurityToProject(): void
|
||||
{
|
||||
try {
|
||||
/**
|
||||
* Create 'documentSecurity' column
|
||||
*/
|
||||
$this->pdo->prepare("ALTER TABLE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}__metadata` ADD COLUMN IF NOT EXISTS documentSecurity TINYINT(1);")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Set 'documentSecurity' column to 1 if NULL
|
||||
*/
|
||||
$this->pdo->prepare("UPDATE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}__metadata` SET documentSecurity = 1 WHERE documentSecurity IS NULL")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ class CNAME extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$records) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
if (isset($record['target']) && $record['target'] === $this->target) {
|
||||
return true;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* Host
|
||||
*
|
||||
* Validate that a host is allowed from given whitelisted hosts list
|
||||
*
|
||||
* @package Utopia\Validator
|
||||
*/
|
||||
class Host extends Validator
|
||||
{
|
||||
protected $whitelist = [];
|
||||
|
||||
/**
|
||||
* @param array $whitelist
|
||||
*/
|
||||
public function __construct(array $whitelist)
|
||||
{
|
||||
$this->whitelist = $whitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'URL host must be one of: ' . \implode(', ', $this->whitelist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass when $value starts with one of the given hosts
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
// Check if value is valid URL
|
||||
$urlValidator = new URL();
|
||||
|
||||
if (!$urlValidator->isValid($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hostname = \parse_url($value, PHP_URL_HOST);
|
||||
$hostnameValidator = new Hostname($this->whitelist);
|
||||
return $hostnameValidator->isValid($hostname);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Exception;
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* IP
|
||||
*
|
||||
* Validate that an variable is a valid IP address
|
||||
*
|
||||
* @package Utopia\Validator
|
||||
*/
|
||||
class IP extends Validator
|
||||
{
|
||||
public const ALL = 'all';
|
||||
public const V4 = 'ipv4';
|
||||
public const V6 = 'ipv6';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = self::ALL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Set a the type of IP check.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(string $type = self::ALL)
|
||||
{
|
||||
if (!in_array($type, [self::ALL, self::V4, self::V6])) {
|
||||
throw new Exception('Unsupported IP type');
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must be a valid IP address';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass when $value is valid IP address.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
switch ($this->type) {
|
||||
case self::ALL:
|
||||
if (\filter_var($value, FILTER_VALIDATE_IP)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::V4:
|
||||
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::V6:
|
||||
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class Origin extends Validator
|
||||
public const CLIENT_TYPE_FLUTTER_MACOS = 'flutter-macos';
|
||||
public const CLIENT_TYPE_FLUTTER_WINDOWS = 'flutter-windows';
|
||||
public const CLIENT_TYPE_FLUTTER_LINUX = 'flutter-linux';
|
||||
public const CLIENT_TYPE_FLUTTER_WEB = 'flutter-web';
|
||||
public const CLIENT_TYPE_APPLE_IOS = 'apple-ios';
|
||||
public const CLIENT_TYPE_APPLE_MACOS = 'apple-macos';
|
||||
public const CLIENT_TYPE_APPLE_WATCHOS = 'apple-watchos';
|
||||
@@ -25,8 +26,10 @@ class Origin extends Validator
|
||||
public const SCHEME_TYPE_HTTP = 'http';
|
||||
public const SCHEME_TYPE_HTTPS = 'https';
|
||||
public const SCHEME_TYPE_IOS = 'appwrite-ios';
|
||||
public const SCHEME_TYPE_ANDROID = 'appwrite-android';
|
||||
public const SCHEME_TYPE_MACOS = 'appwrite-macos';
|
||||
public const SCHEME_TYPE_WATCHOS = 'appwrite-watchos';
|
||||
public const SCHEME_TYPE_TVOS = 'appwrite-tvos';
|
||||
public const SCHEME_TYPE_ANDROID = 'appwrite-android';
|
||||
public const SCHEME_TYPE_WINDOWS = 'appwrite-windows';
|
||||
public const SCHEME_TYPE_LINUX = 'appwrite-linux';
|
||||
|
||||
@@ -37,8 +40,10 @@ class Origin extends Validator
|
||||
self::SCHEME_TYPE_HTTP => 'Web',
|
||||
self::SCHEME_TYPE_HTTPS => 'Web',
|
||||
self::SCHEME_TYPE_IOS => 'iOS',
|
||||
self::SCHEME_TYPE_ANDROID => 'Android',
|
||||
self::SCHEME_TYPE_MACOS => 'macOS',
|
||||
self::SCHEME_TYPE_WATCHOS => 'watchOS',
|
||||
self::SCHEME_TYPE_TVOS => 'tvOS',
|
||||
self::SCHEME_TYPE_ANDROID => 'Android',
|
||||
self::SCHEME_TYPE_WINDOWS => 'Windows',
|
||||
self::SCHEME_TYPE_LINUX => 'Linux',
|
||||
];
|
||||
@@ -69,6 +74,7 @@ class Origin extends Validator
|
||||
|
||||
switch ($type) {
|
||||
case self::CLIENT_TYPE_WEB:
|
||||
case self::CLIENT_TYPE_FLUTTER_WEB:
|
||||
$this->clients[] = (isset($platform['hostname'])) ? $platform['hostname'] : '';
|
||||
break;
|
||||
|
||||
@@ -79,6 +85,9 @@ class Origin extends Validator
|
||||
case self::CLIENT_TYPE_FLUTTER_LINUX:
|
||||
case self::CLIENT_TYPE_ANDROID:
|
||||
case self::CLIENT_TYPE_APPLE_IOS:
|
||||
case self::CLIENT_TYPE_APPLE_MACOS:
|
||||
case self::CLIENT_TYPE_APPLE_WATCHOS:
|
||||
case self::CLIENT_TYPE_APPLE_TVOS:
|
||||
$this->clients[] = (isset($platform['key'])) ? $platform['key'] : '';
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* URL
|
||||
*
|
||||
* Validate that an variable is a valid URL
|
||||
*
|
||||
* @package Appwrite\Network\Validator
|
||||
*/
|
||||
class URL extends Validator
|
||||
{
|
||||
protected array $allowedSchemes;
|
||||
|
||||
/**
|
||||
* @param array $allowedSchemes
|
||||
*/
|
||||
public function __construct(array $allowedSchemes = [])
|
||||
{
|
||||
$this->allowedSchemes = $allowedSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
if (!empty($this->allowedSchemes)) {
|
||||
return 'Value must be a valid URL with following schemes (' . \implode(', ', $this->allowedSchemes) . ')';
|
||||
}
|
||||
|
||||
return 'Value must be a valid URL';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass when $value is valid URL.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
$sanitizedURL = '';
|
||||
|
||||
foreach (str_split($value) as $character) {
|
||||
$sanitizedURL .= (ord($character) > 127) ? rawurlencode($character) : $character;
|
||||
}
|
||||
|
||||
if (\filter_var($sanitizedURL, FILTER_VALIDATE_URL) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($this->allowedSchemes) && !\in_array(\parse_url($sanitizedURL, PHP_URL_SCHEME), $this->allowedSchemes)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Promises;
|
||||
|
||||
abstract class Promise
|
||||
{
|
||||
protected const STATE_PENDING = 1;
|
||||
protected const STATE_FULFILLED = 0;
|
||||
protected const STATE_REJECTED = -1;
|
||||
|
||||
protected int $state = self::STATE_PENDING;
|
||||
|
||||
private mixed $result;
|
||||
|
||||
public function __construct(?callable $executor = null)
|
||||
{
|
||||
if (\is_null($executor)) {
|
||||
return;
|
||||
}
|
||||
$resolve = function ($value) {
|
||||
$this->setResult($value);
|
||||
$this->setState(self::STATE_FULFILLED);
|
||||
};
|
||||
$reject = function ($value) {
|
||||
$this->setResult($value);
|
||||
$this->setState(self::STATE_REJECTED);
|
||||
};
|
||||
$this->execute($executor, $resolve, $reject);
|
||||
}
|
||||
|
||||
abstract protected function execute(
|
||||
callable $executor,
|
||||
callable $resolve,
|
||||
callable $reject
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Create a new promise from the given callable.
|
||||
*
|
||||
* @param callable $promise
|
||||
* @return self
|
||||
*/
|
||||
public static function create(callable $promise): self
|
||||
{
|
||||
return new static($promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve promise with given value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return self
|
||||
*/
|
||||
public static function resolve(mixed $value): self
|
||||
{
|
||||
return new static(function (callable $resolve) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the promise with the given reason.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return self
|
||||
*/
|
||||
public static function reject(mixed $value): self
|
||||
{
|
||||
return new static(function (callable $resolve, callable $reject) use ($value) {
|
||||
$reject($value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch any exception thrown by the executor.
|
||||
*
|
||||
* @param callable $onRejected
|
||||
* @return self
|
||||
*/
|
||||
public function catch(callable $onRejected): self
|
||||
{
|
||||
return $this->then(null, $onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the promise.
|
||||
*
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
* @return self
|
||||
*/
|
||||
public function then(
|
||||
?callable $onFulfilled = null,
|
||||
?callable $onRejected = null
|
||||
): self {
|
||||
if ($this->isRejected() && $onRejected === null) {
|
||||
return $this;
|
||||
}
|
||||
if ($this->isFulfilled() && $onFulfilled === null) {
|
||||
return $this;
|
||||
}
|
||||
return self::create(function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected) {
|
||||
while ($this->isPending()) {
|
||||
usleep(25000);
|
||||
}
|
||||
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
|
||||
if (!\is_callable($callable)) {
|
||||
$resolve($this->result);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$resolve($callable($this->result));
|
||||
} catch (\Throwable $error) {
|
||||
$reject($error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that completes when all passed in promises complete.
|
||||
*
|
||||
* @param iterable|self[] $promises
|
||||
* @return self
|
||||
*/
|
||||
abstract public static function all(iterable $promises): self;
|
||||
|
||||
/**
|
||||
* Set resolved result
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
protected function setResult(mixed $value): void
|
||||
{
|
||||
if (!\is_callable([$value, 'then'])) {
|
||||
$this->result = $value;
|
||||
return;
|
||||
}
|
||||
|
||||
$resolved = false;
|
||||
|
||||
$callable = function ($value) use (&$resolved) {
|
||||
$this->setResult($value);
|
||||
$resolved = true;
|
||||
};
|
||||
|
||||
$value->then($callable, $callable);
|
||||
|
||||
while (!$resolved) {
|
||||
usleep(25000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change promise state
|
||||
*
|
||||
* @param integer $state
|
||||
* @return void
|
||||
*/
|
||||
protected function setState(int $state): void
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise is pending
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isPending(): bool
|
||||
{
|
||||
return $this->state == self::STATE_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise is fulfilled
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isFulfilled(): bool
|
||||
{
|
||||
return $this->state == self::STATE_FULFILLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise is rejected
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isRejected(): bool
|
||||
{
|
||||
return $this->state == self::STATE_REJECTED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Promises;
|
||||
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
class Swoole extends Promise
|
||||
{
|
||||
public function __construct(?callable $executor = null)
|
||||
{
|
||||
parent::__construct($executor);
|
||||
}
|
||||
|
||||
protected function execute(
|
||||
callable $executor,
|
||||
callable $resolve,
|
||||
callable $reject
|
||||
): void {
|
||||
\go(function () use ($executor, $resolve, $reject) {
|
||||
try {
|
||||
$executor($resolve, $reject);
|
||||
} catch (\Throwable $exception) {
|
||||
$reject($exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that completes when all passed in promises complete.
|
||||
*
|
||||
* @param iterable|Swoole[] $promises
|
||||
* @return Promise
|
||||
*/
|
||||
public static function all(iterable $promises): Promise
|
||||
{
|
||||
return self::create(function (callable $resolve, callable $reject) use ($promises) {
|
||||
$ticks = count($promises);
|
||||
|
||||
$result = [];
|
||||
$error = null;
|
||||
$channel = new Channel($ticks);
|
||||
$key = 0;
|
||||
|
||||
foreach ($promises as $promise) {
|
||||
$promise->then(function ($value) use ($key, &$result, $channel) {
|
||||
$result[$key] = $value;
|
||||
$channel->push(true);
|
||||
return $value;
|
||||
}, function ($err) use ($channel, &$error) {
|
||||
$channel->push(true);
|
||||
if ($error === null) {
|
||||
$error = $err;
|
||||
}
|
||||
});
|
||||
$key++;
|
||||
}
|
||||
while ($ticks--) {
|
||||
$channel->pop();
|
||||
}
|
||||
$channel->close();
|
||||
|
||||
if ($error !== null) {
|
||||
$reject($error);
|
||||
return;
|
||||
}
|
||||
|
||||
$resolve($result);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,23 @@
|
||||
|
||||
namespace Appwrite\Resque;
|
||||
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Device\Backblaze;
|
||||
use Utopia\Storage\Device\DOSpaces;
|
||||
use Utopia\Storage\Device\Linode;
|
||||
use Utopia\Storage\Device\Wasabi;
|
||||
use Utopia\Storage\Device\Backblaze;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Device\S3;
|
||||
use Exception;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Storage\Device\Wasabi;
|
||||
use Utopia\Storage\Storage;
|
||||
|
||||
abstract class Worker
|
||||
{
|
||||
@@ -53,7 +54,7 @@ abstract class Worker
|
||||
* @return void
|
||||
* @throws \Exception|\Throwable
|
||||
*/
|
||||
public function init()
|
||||
public function init(): void
|
||||
{
|
||||
throw new Exception("Please implement init method in worker");
|
||||
}
|
||||
@@ -65,7 +66,7 @@ abstract class Worker
|
||||
* @return void
|
||||
* @throws \Exception|\Throwable
|
||||
*/
|
||||
public function run()
|
||||
public function run(): void
|
||||
{
|
||||
throw new Exception("Please implement run method in worker");
|
||||
}
|
||||
@@ -77,7 +78,7 @@ abstract class Worker
|
||||
* @return void
|
||||
* @throws \Exception|\Throwable
|
||||
*/
|
||||
public function shutdown()
|
||||
public function shutdown(): void
|
||||
{
|
||||
throw new Exception("Please implement shutdown method in worker");
|
||||
}
|
||||
@@ -151,35 +152,39 @@ abstract class Worker
|
||||
/**
|
||||
* Register callback. Will be executed when error occurs.
|
||||
* @param callable $callback
|
||||
* @param Throwable $error
|
||||
* @return self
|
||||
* @return void
|
||||
*/
|
||||
public static function error(callable $callback): void
|
||||
{
|
||||
\array_push(self::$errorCallbacks, $callback);
|
||||
self::$errorCallbacks[] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get internal project database
|
||||
* @param string $projectId
|
||||
* @return Database
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getProjectDB(string $projectId): Database
|
||||
protected function getProjectDB(string $projectId, ?Document $project = null): Database
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
if ($project === null) {
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
|
||||
if ($projectId === 'console') {
|
||||
return $consoleDB;
|
||||
if ($projectId === 'console') {
|
||||
return $consoleDB;
|
||||
}
|
||||
|
||||
/** @var Document $project */
|
||||
$project = Authorization::skip(fn() => $consoleDB->getDocument('projects', $projectId));
|
||||
}
|
||||
|
||||
/** @var Document $project */
|
||||
$project = Authorization::skip(fn() => $consoleDB->getDocument('projects', $projectId));
|
||||
|
||||
return $this->getDB(self::DATABASE_PROJECT, $projectId, $project->getInternalId());
|
||||
return $this->getDB(self::DATABASE_PROJECT, $projectId, $project->getInternalId(), $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get console database
|
||||
* @return Database
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getConsoleDB(): Database
|
||||
{
|
||||
@@ -187,24 +192,35 @@ abstract class Worker
|
||||
}
|
||||
|
||||
/**
|
||||
* Get console database
|
||||
* @param string $type One of (internal, external, console)
|
||||
* @param string $projectId of internal or external DB
|
||||
* 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
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getDB(string $type, string $projectId = '', string $projectInternalId = ''): Database
|
||||
{
|
||||
private function getDB(
|
||||
string $type,
|
||||
string $projectId = '',
|
||||
string $projectInternalId = '',
|
||||
?Document $project = null
|
||||
): Database {
|
||||
global $register;
|
||||
|
||||
$namespace = '';
|
||||
$sleep = DATABASE_RECONNECT_SLEEP; // overwritten when necessary
|
||||
|
||||
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}";
|
||||
$namespace = "_$projectInternalId";
|
||||
break;
|
||||
case self::DATABASE_CONSOLE:
|
||||
$namespace = "_console";
|
||||
@@ -212,12 +228,11 @@ abstract class Worker
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Unknown database type: ' . $type);
|
||||
break;
|
||||
}
|
||||
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
while (true) {
|
||||
try {
|
||||
$attempts++;
|
||||
$cache = new Cache(new RedisCache($register->get('cache')));
|
||||
@@ -225,8 +240,12 @@ abstract class Worker
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace($namespace); // Main DB
|
||||
|
||||
if (!empty($projectId) && !$database->getDocument('projects', $projectId)->isEmpty()) {
|
||||
throw new \Exception("Project does not exist: {$projectId}");
|
||||
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)) {
|
||||
@@ -235,13 +254,13 @@ abstract class Worker
|
||||
|
||||
break; // leave loop if successful
|
||||
} catch (\Exception $e) {
|
||||
Console::warning("Database not ready. Retrying connection ({$attempts})...");
|
||||
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);
|
||||
}
|
||||
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
@@ -251,7 +270,7 @@ abstract class Worker
|
||||
* @param string $projectId of the project
|
||||
* @return Device
|
||||
*/
|
||||
protected function getFunctionsDevice($projectId): Device
|
||||
protected function getFunctionsDevice(string $projectId): Device
|
||||
{
|
||||
return $this->getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $projectId);
|
||||
}
|
||||
@@ -261,7 +280,7 @@ abstract class Worker
|
||||
* @param string $projectId of the project
|
||||
* @return Device
|
||||
*/
|
||||
protected function getFilesDevice($projectId): Device
|
||||
protected function getFilesDevice(string $projectId): Device
|
||||
{
|
||||
return $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
|
||||
}
|
||||
@@ -272,19 +291,24 @@ abstract class Worker
|
||||
* @param string $projectId of the project
|
||||
* @return Device
|
||||
*/
|
||||
protected function getBuildsDevice($projectId): Device
|
||||
protected function getBuildsDevice(string $projectId): Device
|
||||
{
|
||||
return $this->getDevice(APP_STORAGE_BUILDS . '/app-' . $projectId);
|
||||
}
|
||||
|
||||
protected function getCacheDevice(string $projectId): Device
|
||||
{
|
||||
return $this->getDevice(APP_STORAGE_CACHE . '/app-' . $projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device based on selected storage environment
|
||||
* @param string $root path of the device
|
||||
* @return Device
|
||||
*/
|
||||
public function getDevice($root): Device
|
||||
public function getDevice(string $root): Device
|
||||
{
|
||||
switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) {
|
||||
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS;
|
||||
|
||||
abstract class Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $secret;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function __construct(string $user, string $secret)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Message to phone.
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
abstract public function send(string $from, string $to, string $message): void;
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $headers
|
||||
* @param string $payload
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function request(string $method, string $url, array $headers = [], ?string $payload = null, ?string $userpwd = null): string
|
||||
{
|
||||
$ch = \curl_init($url);
|
||||
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
\curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
\curl_setopt($ch, CURLOPT_USERAGENT, 'Appwrite Phone Authentication');
|
||||
|
||||
if (!is_null($payload)) {
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
}
|
||||
|
||||
if (!is_null($userpwd)) {
|
||||
\curl_setopt($ch, CURLOPT_USERPWD, $userpwd);
|
||||
}
|
||||
|
||||
$headers[] = 'Content-length: ' . \strlen($payload);
|
||||
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$response = (string) \curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($code >= 400) {
|
||||
throw new \Exception($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Mock adapter used to E2E test worker
|
||||
class Mock extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'http://request-catcher:5000/mock-sms';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint,
|
||||
payload: \json_encode([
|
||||
'message' => $message,
|
||||
'from' => $from,
|
||||
'to' => $to
|
||||
]),
|
||||
headers: [
|
||||
"content-type: application/json",
|
||||
"x-username: {$this->user}",
|
||||
"x-key: {$this->secret}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Reference Material
|
||||
// https://docs.msg91.com/p/tf9GTextN/e/Irz7-x1PK/MSG91
|
||||
|
||||
class Msg91 extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://api.msg91.com/api/v5/flow/';
|
||||
|
||||
/**
|
||||
* For Flow based sending SMS sender ID should not be set in flow
|
||||
* In environment _APP_SMS_PROVIDER format is 'sms://[senderID]:[authKey]@msg91'.
|
||||
* _APP_SMS_FROM value is flow ID created in Msg91
|
||||
* Eg. _APP_SMS_PROVIDER = sms://DINESH:5e1e93cad6fc054d8e759a5b@msg91
|
||||
* _APP_SMS_FROM = 3968636f704b303135323339
|
||||
* @param string $from-> utilized from for flow id
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint,
|
||||
payload: json_encode([
|
||||
'sender' => $this->user,
|
||||
'otp' => $message,
|
||||
'flow_id' => $from,
|
||||
'mobiles' => $to
|
||||
]),
|
||||
headers: [
|
||||
"content-type: application/JSON",
|
||||
"authkey: {$this->secret}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Reference Material
|
||||
// https://developer.telesign.com/enterprise/docs/sms-api-send-an-sms
|
||||
|
||||
class Telesign extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://rest-api.telesign.com/v1/messaging';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
* @throws \Appwrite\Extend\Exception
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint,
|
||||
payload: \http_build_query([
|
||||
'message' => $message,
|
||||
'message_type' => 'otp',
|
||||
'phone_number' => $to
|
||||
]),
|
||||
userpwd: "{$this->user}:{$this->secret}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Reference Material
|
||||
// https://www.textmagic.com/docs/api/start/
|
||||
|
||||
class TextMagic extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://rest.textmagic.com/api/v2';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
$from = ltrim($from, '+');
|
||||
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint . '/messages',
|
||||
payload: \http_build_query([
|
||||
'text' => $message,
|
||||
'from' => $from,
|
||||
'phones' => $to
|
||||
]),
|
||||
headers: [
|
||||
"X-TM-Username: {$this->user}",
|
||||
"X-TM-Key: {$this->secret}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Reference Material
|
||||
// https://www.twilio.com/docs/sms/api
|
||||
|
||||
class Twilio extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://api.twilio.com/2010-04-01';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: "{$this->endpoint}/Accounts/{$this->user}/Messages.json",
|
||||
payload: \http_build_query([
|
||||
'Body' => $message,
|
||||
'From' => $from,
|
||||
'To' => $to
|
||||
]),
|
||||
userpwd: "{$this->user}:{$this->secret}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\SMS\Adapter;
|
||||
|
||||
use Appwrite\SMS\Adapter;
|
||||
|
||||
// Reference Material
|
||||
// https://developer.vonage.com/api/sms
|
||||
|
||||
class Vonage extends Adapter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://rest.nexmo.com/sms/json';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
$headers = ['Content-Type: application/x-www-form-urlencoded'];
|
||||
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint,
|
||||
headers: $headers,
|
||||
payload: \http_build_query([
|
||||
'text' => $message,
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'api_key' => $this->user,
|
||||
'api_secret' => $this->secret
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@ namespace Appwrite\Specification\Format;
|
||||
use Appwrite\Specification\Format;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Permission;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class OpenAPI3 extends Format
|
||||
{
|
||||
@@ -170,6 +171,9 @@ class OpenAPI3 extends Format
|
||||
'scope' => $route->getLabel('scope', ''),
|
||||
'platforms' => $sdkPlatforms,
|
||||
'packaging' => $route->getLabel('sdk.packaging', false),
|
||||
'offline-model' => $route->getLabel('sdk.offline.model', ''),
|
||||
'offline-key' => $route->getLabel('sdk.offline.key', ''),
|
||||
'offline-response-key' => $route->getLabel('sdk.offline.response.key', '$id'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -281,6 +285,13 @@ class OpenAPI3 extends Format
|
||||
}
|
||||
}
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
if ($isNullable) {
|
||||
/** @var Nullable $validator */
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Utopia\Validator\Text':
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
@@ -311,7 +322,7 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['format'] = 'email';
|
||||
$node['schema']['x-example'] = 'email@example.com';
|
||||
break;
|
||||
case 'Appwrite\Network\Validator\URL':
|
||||
case 'Utopia\Validator\URL':
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['format'] = 'url';
|
||||
$node['schema']['x-example'] = 'https://example.com';
|
||||
@@ -391,7 +402,7 @@ class OpenAPI3 extends Format
|
||||
case 'Utopia\Validator\Length':
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
break;
|
||||
case 'Appwrite\Network\Validator\Host':
|
||||
case 'Utopia\Validator\Host':
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['format'] = 'url';
|
||||
$node['schema']['x-example'] = 'https://example.com';
|
||||
@@ -446,6 +457,10 @@ class OpenAPI3 extends Format
|
||||
if ($node['x-global'] ?? false) {
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
|
||||
if ($isNullable) {
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
|
||||
|
||||
@@ -5,9 +5,10 @@ namespace Appwrite\Specification\Format;
|
||||
use Appwrite\Specification\Format;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Permission;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
class Swagger2 extends Format
|
||||
{
|
||||
@@ -171,6 +172,9 @@ class Swagger2 extends Format
|
||||
'scope' => $route->getLabel('scope', ''),
|
||||
'platforms' => $sdkPlatforms,
|
||||
'packaging' => $route->getLabel('sdk.packaging', false),
|
||||
'offline-model' => $route->getLabel('sdk.offline.model', ''),
|
||||
'offline-key' => $route->getLabel('sdk.offline.key', ''),
|
||||
'offline-response-key' => $route->getLabel('sdk.offline.response.key', '$id'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -261,7 +265,12 @@ class Swagger2 extends Format
|
||||
|
||||
$bodyRequired = [];
|
||||
|
||||
foreach ($route->getParams() as $name => $param) { // Set params
|
||||
$parameters = \array_merge(
|
||||
$route->getParams(),
|
||||
$route->getLabel('sdk.parameters', []),
|
||||
);
|
||||
|
||||
foreach ($parameters as $name => $param) { // Set params
|
||||
/** @var \Utopia\Validator $validator */
|
||||
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
|
||||
|
||||
@@ -277,6 +286,13 @@ class Swagger2 extends Format
|
||||
}
|
||||
}
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
if ($isNullable) {
|
||||
/** @var Nullable $validator */
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Utopia\Validator\Text':
|
||||
$node['type'] = $validator->getType();
|
||||
@@ -307,7 +323,7 @@ class Swagger2 extends Format
|
||||
$node['format'] = 'email';
|
||||
$node['x-example'] = 'email@example.com';
|
||||
break;
|
||||
case 'Appwrite\Network\Validator\URL':
|
||||
case 'Utopia\Validator\URL':
|
||||
$node['type'] = $validator->getType();
|
||||
$node['format'] = 'url';
|
||||
$node['x-example'] = 'https://example.com';
|
||||
@@ -388,7 +404,7 @@ class Swagger2 extends Format
|
||||
case 'Utopia\Validator\Length':
|
||||
$node['type'] = $validator->getType();
|
||||
break;
|
||||
case 'Appwrite\Network\Validator\Host':
|
||||
case 'Utopia\Validator\Host':
|
||||
$node['type'] = $validator->getType();
|
||||
$node['format'] = 'url';
|
||||
$node['x-example'] = 'https://example.com';
|
||||
@@ -440,6 +456,10 @@ class Swagger2 extends Format
|
||||
$body['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
|
||||
if ($isNullable) {
|
||||
$body['schema']['properties'][$name]['x-nullable'] = true;
|
||||
}
|
||||
|
||||
if (\array_key_exists('items', $node)) {
|
||||
$body['schema']['properties'][$name]['items'] = $node['items'];
|
||||
}
|
||||
|
||||
@@ -4,5 +4,12 @@ namespace Appwrite\Usage;
|
||||
|
||||
abstract class Calculator
|
||||
{
|
||||
protected string $region;
|
||||
|
||||
public function __construct(string $region)
|
||||
{
|
||||
$this->region = $region;
|
||||
}
|
||||
|
||||
abstract public function collect(): void;
|
||||
}
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Usage\Calculators;
|
||||
|
||||
use DateTime;
|
||||
use Utopia\Database\Database as UtopiaDatabase;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class Aggregator extends Database
|
||||
{
|
||||
protected function aggregateDatabaseMetrics(string $projectId): void
|
||||
{
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
$databasesGeneralMetrics = [
|
||||
'databases.$all.requests.create',
|
||||
'databases.$all.requests.read',
|
||||
'databases.$all.requests.update',
|
||||
'databases.$all.requests.delete',
|
||||
'collections.$all.requests.create',
|
||||
'collections.$all.requests.read',
|
||||
'collections.$all.requests.update',
|
||||
'collections.$all.requests.delete',
|
||||
'documents.$all.requests.create',
|
||||
'documents.$all.requests.read',
|
||||
'documents.$all.requests.update',
|
||||
'documents.$all.requests.delete'
|
||||
];
|
||||
|
||||
foreach ($databasesGeneralMetrics as $metric) {
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
|
||||
$databasesDatabaseMetrics = [
|
||||
'collections.databaseId.requests.create',
|
||||
'collections.databaseId.requests.read',
|
||||
'collections.databaseId.requests.update',
|
||||
'collections.databaseId.requests.delete',
|
||||
'documents.databaseId.requests.create',
|
||||
'documents.databaseId.requests.read',
|
||||
'documents.databaseId.requests.update',
|
||||
'documents.databaseId.requests.delete',
|
||||
];
|
||||
|
||||
$this->foreachDocument($projectId, 'databases', [], function (Document $database) use ($databasesDatabaseMetrics, $projectId) {
|
||||
$databaseId = $database->getId();
|
||||
foreach ($databasesDatabaseMetrics as $metric) {
|
||||
$metric = str_replace('databaseId', $databaseId, $metric);
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
|
||||
$databasesCollectionMetrics = [
|
||||
'documents.' . $databaseId . '/collectionId.requests.create',
|
||||
'documents.' . $databaseId . '/collectionId.requests.read',
|
||||
'documents.' . $databaseId . '/collectionId.requests.update',
|
||||
'documents.' . $databaseId . '/collectionId.requests.delete',
|
||||
];
|
||||
|
||||
$this->foreachDocument($projectId, 'database_' . $database->getInternalId(), [], function (Document $collection) use ($databasesCollectionMetrics, $projectId) {
|
||||
$collectionId = $collection->getId();
|
||||
foreach ($databasesCollectionMetrics as $metric) {
|
||||
$metric = str_replace('collectionId', $collectionId, $metric);
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected function aggregateStorageMetrics(string $projectId): void
|
||||
{
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
$storageGeneralMetrics = [
|
||||
'buckets.$all.requests.create',
|
||||
'buckets.$all.requests.read',
|
||||
'buckets.$all.requests.update',
|
||||
'buckets.$all.requests.delete',
|
||||
'files.$all.requests.create',
|
||||
'files.$all.requests.read',
|
||||
'files.$all.requests.update',
|
||||
'files.$all.requests.delete',
|
||||
];
|
||||
|
||||
foreach ($storageGeneralMetrics as $metric) {
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
|
||||
$storageBucketMetrics = [
|
||||
'files.bucketId.requests.create',
|
||||
'files.bucketId.requests.read',
|
||||
'files.bucketId.requests.update',
|
||||
'files.bucketId.requests.delete',
|
||||
];
|
||||
|
||||
$this->foreachDocument($projectId, 'buckets', [], function (Document $bucket) use ($storageBucketMetrics, $projectId) {
|
||||
$bucketId = $bucket->getId();
|
||||
foreach ($storageBucketMetrics as $metric) {
|
||||
$metric = str_replace('bucketId', $bucketId, $metric);
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function aggregateFunctionMetrics(string $projectId): void
|
||||
{
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
$functionsGeneralMetrics = [
|
||||
'project.$all.compute.total',
|
||||
'project.$all.compute.time',
|
||||
'executions.$all.compute.total',
|
||||
'executions.$all.compute.success',
|
||||
'executions.$all.compute.failure',
|
||||
'executions.$all.compute.time',
|
||||
'builds.$all.compute.total',
|
||||
'builds.$all.compute.success',
|
||||
'builds.$all.compute.failure',
|
||||
'builds.$all.compute.time',
|
||||
];
|
||||
|
||||
foreach ($functionsGeneralMetrics as $metric) {
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
|
||||
$functionMetrics = [
|
||||
'executions.functionId.compute.total',
|
||||
'executions.functionId.compute.success',
|
||||
'executions.functionId.compute.failure',
|
||||
'executions.functionId.compute.time',
|
||||
'builds.functionId.compute.total',
|
||||
'builds.functionId.compute.success',
|
||||
'builds.functionId.compute.failure',
|
||||
'builds.functionId.compute.time',
|
||||
];
|
||||
|
||||
$this->foreachDocument($projectId, 'functions', [], function (Document $function) use ($functionMetrics, $projectId) {
|
||||
$functionId = $function->getId();
|
||||
foreach ($functionMetrics as $metric) {
|
||||
$metric = str_replace('functionId', $functionId, $metric);
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function aggregateUsersMetrics(string $projectId): void
|
||||
{
|
||||
$metrics = [
|
||||
'users.$all.requests.create',
|
||||
'users.$all.requests.read',
|
||||
'users.$all.requests.update',
|
||||
'users.$all.requests.delete',
|
||||
'sessions.$all.requests.create',
|
||||
'sessions.$all.requests.delete'
|
||||
];
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$this->aggregateDailyMetric($projectId, $metric);
|
||||
$this->aggregateMonthlyMetric($projectId, $metric);
|
||||
}
|
||||
}
|
||||
|
||||
protected function aggregateGeneralMetrics(string $projectId): void
|
||||
{
|
||||
$this->aggregateDailyMetric($projectId, 'project.$all.network.requests');
|
||||
$this->aggregateDailyMetric($projectId, 'project.$all.network.bandwidth');
|
||||
$this->aggregateDailyMetric($projectId, 'project.$all.network.inbound');
|
||||
$this->aggregateDailyMetric($projectId, 'project.$all.network.outbound');
|
||||
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.requests');
|
||||
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.bandwidth');
|
||||
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.inbound');
|
||||
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.outbound');
|
||||
}
|
||||
|
||||
protected function aggregateDailyMetric(string $projectId, string $metric): void
|
||||
{
|
||||
$beginOfDay = DateTime::createFromFormat('Y-m-d\TH:i:s.v', \date('Y-m-d\T00:00:00.000'))->format(DateTime::RFC3339);
|
||||
$endOfDay = DateTime::createFromFormat('Y-m-d\TH:i:s.v', \date('Y-m-d\T23:59:59.999'))->format(DateTime::RFC3339);
|
||||
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
$value = (int) $this->database->sum('stats', 'value', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['30m']),
|
||||
Query::greaterThanEqual('time', $beginOfDay),
|
||||
Query::lessThanEqual('time', $endOfDay),
|
||||
]);
|
||||
$this->createOrUpdateMetric($projectId, $metric, '1d', $beginOfDay, $value);
|
||||
}
|
||||
|
||||
protected function aggregateMonthlyMetric(string $projectId, string $metric): void
|
||||
{
|
||||
$beginOfMonth = DateTime::createFromFormat('Y-m-d\TH:i:s.v', \date('Y-m-01\T00:00:00.000'))->format(DateTime::RFC3339);
|
||||
$endOfMonth = DateTime::createFromFormat('Y-m-d\TH:i:s.v', \date('Y-m-t\T23:59:59.999'))->format(DateTime::RFC3339);
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
$value = (int) $this->database->sum('stats', 'value', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['1d']),
|
||||
Query::greaterThanEqual('time', $beginOfMonth),
|
||||
Query::lessThanEqual('time', $endOfMonth),
|
||||
]);
|
||||
$this->createOrUpdateMetric($projectId, $metric, '1mo', $beginOfMonth, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect Stats
|
||||
* Collect all database related stats
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function collect(): void
|
||||
{
|
||||
$this->foreachDocument('console', 'projects', [], function (Document $project) {
|
||||
$projectId = $project->getInternalId();
|
||||
|
||||
// Aggregate new metrics from already collected usage metrics
|
||||
// for lower time period (1day and 1 month metric from 30 minute metrics)
|
||||
$this->aggregateGeneralMetrics($projectId);
|
||||
$this->aggregateFunctionMetrics($projectId);
|
||||
$this->aggregateDatabaseMetrics($projectId);
|
||||
$this->aggregateStorageMetrics($projectId);
|
||||
$this->aggregateUsersMetrics($projectId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Usage\Calculators;
|
||||
|
||||
use Exception;
|
||||
use Appwrite\Usage\Calculator;
|
||||
use DateTime;
|
||||
use Utopia\Database\Database as UtopiaDatabase;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Authorization;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class Database extends Calculator
|
||||
{
|
||||
protected array $periods = [
|
||||
[
|
||||
'key' => '30m',
|
||||
'multiplier' => 1800,
|
||||
],
|
||||
[
|
||||
'key' => '1d',
|
||||
'multiplier' => 86400,
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(UtopiaDatabase $database, callable $errorHandler = null)
|
||||
{
|
||||
$this->database = $database;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Per Period Metric
|
||||
*
|
||||
* Create given metric for each defined period
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param string $metric
|
||||
* @param int $value
|
||||
* @param bool $monthly
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
protected function createPerPeriodMetric(string $projectId, string $metric, int $value, bool $monthly = false): void
|
||||
{
|
||||
foreach ($this->periods as $options) {
|
||||
$period = $options['key'];
|
||||
$date = new \DateTime();
|
||||
if ($period === '30m') {
|
||||
$minutes = $date->format('i') >= '30' ? "30" : "00";
|
||||
$time = $date->format('Y-m-d H:' . $minutes . ':00');
|
||||
} elseif ($period === '1d') {
|
||||
$time = $date->format('Y-m-d 00:00:00');
|
||||
} else {
|
||||
throw new Exception("Period type not found", 500);
|
||||
}
|
||||
$this->createOrUpdateMetric($projectId, $metric, $period, $time, $value);
|
||||
}
|
||||
|
||||
// Required for billing
|
||||
if ($monthly) {
|
||||
$time = DateTime::createFromFormat('Y-m-d\TH:i:s.v', \date('Y-m-01\T00:00:00.000'))->format(DateTime::RFC3339);
|
||||
$this->createOrUpdateMetric($projectId, $metric, '1mo', $time, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update Metric
|
||||
*
|
||||
* Create or update each metric in the stats collection for the given project
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param string $metric
|
||||
* @param string $period
|
||||
* @param string $time
|
||||
* @param int $value
|
||||
*
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
protected function createOrUpdateMetric(string $projectId, string $metric, string $period, string $time, int $value): void
|
||||
{
|
||||
$id = \md5("{$time}_{$period}_{$metric}");
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$document = $this->database->getDocument('stats', $id);
|
||||
if ($document->isEmpty()) {
|
||||
$this->database->createDocument('stats', new Document([
|
||||
'$id' => $id,
|
||||
'period' => $period,
|
||||
'time' => $time,
|
||||
'metric' => $metric,
|
||||
'value' => $value,
|
||||
'type' => 2, // these are cumulative metrics
|
||||
]));
|
||||
} else {
|
||||
$this->database->updateDocument(
|
||||
'stats',
|
||||
$document->getId(),
|
||||
$document->setAttribute('value', $value)
|
||||
);
|
||||
}
|
||||
} catch (\Exception$e) { // if projects are deleted this might fail
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreach Document
|
||||
*
|
||||
* Call provided callback for each document in the collection
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param string $collection
|
||||
* @param array $queries
|
||||
* @param callable $callback
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function foreachDocument(string $projectId, string $collection, array $queries, callable $callback): void
|
||||
{
|
||||
$limit = 50;
|
||||
$results = [];
|
||||
$sum = $limit;
|
||||
$latestDocument = null;
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
while ($sum === $limit) {
|
||||
try {
|
||||
$paginationQueries = [Query::limit($limit)];
|
||||
if ($latestDocument !== null) {
|
||||
$paginationQueries[] = Query::cursorAfter($latestDocument);
|
||||
}
|
||||
$results = $this->database->find($collection, \array_merge($paginationQueries, $queries));
|
||||
} catch (\Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "fetch_documents_project_{$projectId}_collection_{$collection}");
|
||||
return;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
if (empty($results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sum = count($results);
|
||||
|
||||
foreach ($results as $document) {
|
||||
if (is_callable($callback)) {
|
||||
$callback($document);
|
||||
}
|
||||
}
|
||||
$latestDocument = $results[array_key_last($results)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum
|
||||
*
|
||||
* Calculate sum of an attribute of documents in collection
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param string $collection
|
||||
* @param string $attribute
|
||||
* @param string|null $metric
|
||||
* @param int $multiplier
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
private function sum(string $projectId, string $collection, string $attribute, string $metric = null, int $multiplier = 1): int
|
||||
{
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$sum = $this->database->sum($collection, $attribute);
|
||||
$sum = (int) ($sum * $multiplier);
|
||||
|
||||
if (!is_null($metric)) {
|
||||
$this->createPerPeriodMetric($projectId, $metric, $sum);
|
||||
}
|
||||
return $sum;
|
||||
} catch (Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "fetch_sum_project_{$projectId}_collection_{$collection}");
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count
|
||||
*
|
||||
* Count number of documents in collection
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param string $collection
|
||||
* @param ?string $metric
|
||||
*
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
private function count(string $projectId, string $collection, ?string $metric = null): int
|
||||
{
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$count = $this->database->count($collection);
|
||||
if (!is_null($metric)) {
|
||||
$this->createPerPeriodMetric($projectId, (string) $metric, $count);
|
||||
}
|
||||
return $count;
|
||||
} catch (Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "fetch_count_project_{$projectId}_collection_{$collection}");
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deployments Total
|
||||
*
|
||||
* Total sum of storage used by deployments
|
||||
*
|
||||
* @param string $projectId
|
||||
*
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deploymentsTotal(string $projectId): int
|
||||
{
|
||||
return $this->sum($projectId, 'deployments', 'size', 'deployments.$all.storage.size');
|
||||
}
|
||||
|
||||
/**
|
||||
* Users Stats
|
||||
*
|
||||
* Metric: users.count
|
||||
*
|
||||
* @param string $projectId
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function usersStats(string $projectId): void
|
||||
{
|
||||
$this->count($projectId, 'users', 'users.$all.count.total');
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage Stats
|
||||
*
|
||||
* Metrics: buckets.$all.count.total, files.$all.count.total, files.bucketId,count.total,
|
||||
* files.$all.storage.size, files.bucketId.storage.size, project.$all.storage.size
|
||||
*
|
||||
* @param string $projectId
|
||||
*
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
private function storageStats(string $projectId): void
|
||||
{
|
||||
$projectFilesTotal = 0;
|
||||
$projectFilesCount = 0;
|
||||
|
||||
$metric = 'buckets.$all.count.total';
|
||||
$this->count($projectId, 'buckets', $metric);
|
||||
|
||||
$this->foreachDocument($projectId, 'buckets', [], function ($bucket) use (&$projectFilesCount, &$projectFilesTotal, $projectId,) {
|
||||
$metric = "files.{$bucket->getId()}.count.total";
|
||||
$count = $this->count($projectId, 'bucket_' . $bucket->getInternalId(), $metric);
|
||||
$projectFilesCount += $count;
|
||||
|
||||
$metric = "files.{$bucket->getId()}.storage.size";
|
||||
$sum = $this->sum($projectId, 'bucket_' . $bucket->getInternalId(), 'sizeOriginal', $metric);
|
||||
$projectFilesTotal += $sum;
|
||||
});
|
||||
|
||||
$this->createPerPeriodMetric($projectId, 'files.$all.count.total', $projectFilesCount);
|
||||
$this->createPerPeriodMetric($projectId, 'files.$all.storage.size', $projectFilesTotal);
|
||||
|
||||
$deploymentsTotal = $this->deploymentsTotal($projectId);
|
||||
$this->createPerPeriodMetric($projectId, 'project.$all.storage.size', $projectFilesTotal + $deploymentsTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database Stats
|
||||
*
|
||||
* Collect all database stats
|
||||
* Metrics: databases.$all.count.total, collections.$all.count.total, collections.databaseId.count.total,
|
||||
* documents.$all.count.all, documents.databaseId.count.total, documents.databaseId/collectionId.count.total
|
||||
*
|
||||
* @param string $projectId
|
||||
*
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
private function databaseStats(string $projectId): void
|
||||
{
|
||||
$projectDocumentsCount = 0;
|
||||
$projectCollectionsCount = 0;
|
||||
|
||||
$this->count($projectId, 'databases', 'databases.$all.count.total');
|
||||
|
||||
$this->foreachDocument($projectId, 'databases', [], function ($database) use (&$projectDocumentsCount, &$projectCollectionsCount, $projectId) {
|
||||
$metric = "collections.{$database->getId()}.count.total";
|
||||
$count = $this->count($projectId, 'database_' . $database->getInternalId(), $metric);
|
||||
$projectCollectionsCount += $count;
|
||||
$databaseDocumentsCount = 0;
|
||||
|
||||
$this->foreachDocument($projectId, 'database_' . $database->getInternalId(), [], function ($collection) use (&$projectDocumentsCount, &$databaseDocumentsCount, $projectId, $database) {
|
||||
$metric = "documents.{$database->getId()}/{$collection->getId()}.count.total";
|
||||
|
||||
$count = $this->count($projectId, 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $metric);
|
||||
$projectDocumentsCount += $count;
|
||||
$databaseDocumentsCount += $count;
|
||||
});
|
||||
|
||||
$this->createPerPeriodMetric($projectId, "documents.{$database->getId()}.count.total", $databaseDocumentsCount);
|
||||
});
|
||||
|
||||
$this->createPerPeriodMetric($projectId, 'collections.$all.count.total', $projectCollectionsCount);
|
||||
$this->createPerPeriodMetric($projectId, 'documents.$all.count.total', $projectDocumentsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect Stats
|
||||
*
|
||||
* Collect all database related stats
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function collect(): void
|
||||
{
|
||||
$this->foreachDocument('console', 'projects', [], function (Document $project) {
|
||||
$projectId = $project->getInternalId();
|
||||
|
||||
$this->usersStats($projectId);
|
||||
$this->databaseStats($projectId);
|
||||
$this->storageStats($projectId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Usage\Calculators;
|
||||
|
||||
use Utopia\App;
|
||||
use Appwrite\Usage\Calculator;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -10,12 +11,54 @@ use DateTime;
|
||||
|
||||
class TimeSeries extends Calculator
|
||||
{
|
||||
/**
|
||||
* InfluxDB
|
||||
*
|
||||
* @var InfluxDatabase
|
||||
*/
|
||||
protected InfluxDatabase $influxDB;
|
||||
|
||||
/**
|
||||
* Utopia Database
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
protected Database $database;
|
||||
|
||||
/**
|
||||
* Error Handler Callback
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected $errorHandler;
|
||||
|
||||
/**
|
||||
* Latest times for metric that was synced to the database
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $latestTime = [];
|
||||
|
||||
// all the mertics that we are collecting
|
||||
/**
|
||||
* Periods the metrics are collected for
|
||||
* @var array
|
||||
*/
|
||||
protected array $periods = [
|
||||
[
|
||||
'key' => '1h',
|
||||
'startTime' => '-24 hours'
|
||||
],
|
||||
[
|
||||
'key' => '1d',
|
||||
'startTime' => '-30 days'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* All the metrics that we are collecting
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $metrics = [
|
||||
'project.$all.network.requests' => [
|
||||
'table' => 'appwrite_usage_project_{scope}_network_requests',
|
||||
@@ -189,12 +232,6 @@ class TimeSeries extends Calculator
|
||||
'executions.$all.compute.total' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute',
|
||||
],
|
||||
'builds.$all.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
],
|
||||
'executions.$all.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
],
|
||||
'builds.$all.compute.total' => [
|
||||
'table' => 'appwrite_usage_builds_{scope}_compute',
|
||||
],
|
||||
@@ -230,14 +267,7 @@ class TimeSeries extends Calculator
|
||||
'table' => 'appwrite_usage_builds_{scope}_compute',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'executions.functionId.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'builds.functionId.compute.time' => [
|
||||
'table' => 'appwrite_usage_builds_{scope}_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
|
||||
'executions.functionId.compute.failure' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute',
|
||||
'groupBy' => ['functionId'],
|
||||
@@ -267,19 +297,94 @@ class TimeSeries extends Calculator
|
||||
],
|
||||
],
|
||||
|
||||
// counters
|
||||
'users.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_users_{scope}_count_total',
|
||||
],
|
||||
'buckets.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_buckets_{scope}_count_total',
|
||||
],
|
||||
'files.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_count_total',
|
||||
],
|
||||
'files.bucketId.count.total' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_count_total',
|
||||
'groupBy' => ['bucketId']
|
||||
],
|
||||
'databases.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_databases_{scope}_count_total',
|
||||
],
|
||||
'collections.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_collections_{scope}_count_total',
|
||||
],
|
||||
'documents.$all.count.total' => [
|
||||
'table' => 'appwrite_usage_documents_{scope}_count_total',
|
||||
],
|
||||
'collections.databaseId.count.total' => [
|
||||
'table' => 'appwrite_usage_collections_{scope}_count_total',
|
||||
'groupBy' => ['databaseId']
|
||||
],
|
||||
'documents.databaseId.count.total' => [
|
||||
'table' => 'appwrite_usage_documents_{scope}_count_total',
|
||||
'groupBy' => ['databaseId']
|
||||
],
|
||||
'documents.databaseId/collectionId.count.total' => [
|
||||
'table' => 'appwrite_usage_documents_{scope}_count_total',
|
||||
'groupBy' => ['databaseId', 'collectionId']
|
||||
],
|
||||
'deployments.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_deployments_{scope}_storage_size',
|
||||
],
|
||||
'project.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_project_{scope}_storage_size',
|
||||
],
|
||||
'files.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_storage_size',
|
||||
],
|
||||
'files.$bucketId.storage.size' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_storage_size',
|
||||
'groupBy' => ['bucketId']
|
||||
],
|
||||
|
||||
'builds.$all.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
],
|
||||
'executions.$all.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
],
|
||||
|
||||
'executions.functionId.compute.time' => [
|
||||
'table' => 'appwrite_usage_executions_{scope}_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'builds.functionId.compute.time' => [
|
||||
'table' => 'appwrite_usage_builds_{scope}_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
|
||||
'project.$all.compute.time' => [ // Built time + execution time
|
||||
'table' => 'appwrite_usage_project_{scope}_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
|
||||
'deployments.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_deployments_{scope}_storage_size'
|
||||
],
|
||||
'project.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_project_{scope}_storage_size'
|
||||
],
|
||||
'files.$all.storage.size' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_storage_size'
|
||||
],
|
||||
'files.bucketId.storage.size' => [
|
||||
'table' => 'appwrite_usage_files_{scope}_storage_size',
|
||||
'groupBy' => ['bucketId']
|
||||
]
|
||||
];
|
||||
|
||||
protected array $period = [
|
||||
'key' => '30m',
|
||||
'startTime' => '-24 hours',
|
||||
];
|
||||
|
||||
public function __construct(Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
|
||||
public function __construct(string $region, Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
|
||||
{
|
||||
parent::__construct($region);
|
||||
$this->database = $database;
|
||||
$this->influxDB = $influxDB;
|
||||
$this->errorHandler = $errorHandler;
|
||||
@@ -301,9 +406,7 @@ 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('_console');
|
||||
$project = $this->database->getDocument('projects', $projectId);
|
||||
$this->database->setNamespace('_' . $project->getInternalId());
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$document = $this->database->getDocument('stats', $id);
|
||||
@@ -315,6 +418,7 @@ class TimeSeries extends Calculator
|
||||
'metric' => $metric,
|
||||
'value' => $value,
|
||||
'type' => $type,
|
||||
'region' => $this->region,
|
||||
]));
|
||||
} else {
|
||||
$this->database->updateDocument(
|
||||
@@ -365,7 +469,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\" {$groupBy} ";
|
||||
$query .= "GROUP BY time({$period['key']}), \"projectId\", \"projectInternalId\" {$groupBy} ";
|
||||
$query .= "FILL(null)";
|
||||
|
||||
try {
|
||||
@@ -387,9 +491,11 @@ class TimeSeries extends Calculator
|
||||
}
|
||||
|
||||
$value = (!empty($point['value'])) ? $point['value'] : 0;
|
||||
|
||||
if (empty($point['projectInternalId'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
$this->createOrUpdateMetric(
|
||||
$projectId,
|
||||
$point['projectInternalId'],
|
||||
$point['time'],
|
||||
$period['key'],
|
||||
$metricUpdated,
|
||||
@@ -416,14 +522,16 @@ class TimeSeries extends Calculator
|
||||
*/
|
||||
public function collect(): void
|
||||
{
|
||||
foreach ($this->metrics as $metric => $options) { //for each metrics
|
||||
try {
|
||||
$this->syncFromInfluxDB($metric, $options, $this->period);
|
||||
} catch (\Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e);
|
||||
} else {
|
||||
throw $e;
|
||||
foreach ($this->periods as $period) {
|
||||
foreach ($this->metrics as $metric => $options) { //for each metrics
|
||||
try {
|
||||
$this->syncFromInfluxDB($metric, $options, $period);
|
||||
} catch (\Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e);
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,14 @@ class Stats
|
||||
|
||||
/**
|
||||
* Submit data to StatsD.
|
||||
* Send various metrics to StatsD based on the parameters that are set
|
||||
* @return void
|
||||
*/
|
||||
public function submit(): void
|
||||
{
|
||||
$projectId = $this->params['projectId'] ?? '';
|
||||
$tags = ",projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$projectInternalId = $this->params['projectInternalId'];
|
||||
$tags = ",projectInternalId={$projectInternalId},projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
// the global namespace is prepended to every key (optional)
|
||||
$this->statsd->setNamespace($this->namespace);
|
||||
@@ -91,8 +94,8 @@ class Stats
|
||||
$this->statsd->increment('project.{scope}.network.requests' . $tags . ',method=' . \strtolower($httpMethod));
|
||||
}
|
||||
|
||||
$inbound = $this->params['networkRequestSize'] ?? 0;
|
||||
$outbound = $this->params['networkResponseSize'] ?? 0;
|
||||
$inbound = $this->params['project.{scope}.network.inbound'] ?? 0;
|
||||
$outbound = $this->params['project.{scope}.network.outbound'] ?? 0;
|
||||
$this->statsd->count('project.{scope}.network.inbound' . $tags, $inbound);
|
||||
$this->statsd->count('project.{scope}.network.outbound' . $tags, $outbound);
|
||||
$this->statsd->count('project.{scope}.network.bandwidth' . $tags, $inbound + $outbound);
|
||||
@@ -102,12 +105,13 @@ class Stats
|
||||
'users.{scope}.requests.read',
|
||||
'users.{scope}.requests.update',
|
||||
'users.{scope}.requests.delete',
|
||||
'users.{scope}.count.total',
|
||||
];
|
||||
|
||||
foreach ($usersMetrics as $metric) {
|
||||
$value = $this->params[$metric] ?? 0;
|
||||
if ($value >= 1) {
|
||||
$this->statsd->increment($metric . $tags);
|
||||
if ($value === 1 || $value === -1) {
|
||||
$this->statsd->count($metric . $tags, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,13 +128,16 @@ class Stats
|
||||
'documents.{scope}.requests.read',
|
||||
'documents.{scope}.requests.update',
|
||||
'documents.{scope}.requests.delete',
|
||||
'databases.{scope}.count.total',
|
||||
'collections.{scope}.count.total',
|
||||
'documents.{scope}.count.total'
|
||||
];
|
||||
|
||||
foreach ($dbMetrics as $metric) {
|
||||
$value = $this->params[$metric] ?? 0;
|
||||
if ($value >= 1) {
|
||||
if ($value === 1 || $value === -1) {
|
||||
$dbTags = $tags . ",collectionId=" . ($this->params['collectionId'] ?? '') . ",databaseId=" . ($this->params['databaseId'] ?? '');
|
||||
$this->statsd->increment($metric . $dbTags);
|
||||
$this->statsd->count($metric . $dbTags, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,13 +150,16 @@ class Stats
|
||||
'files.{scope}.requests.read',
|
||||
'files.{scope}.requests.update',
|
||||
'files.{scope}.requests.delete',
|
||||
'buckets.{scope}.count.total',
|
||||
'files.{scope}.count.total',
|
||||
'files.{scope}.storage.size'
|
||||
];
|
||||
|
||||
foreach ($storageMertics as $metric) {
|
||||
$value = $this->params[$metric] ?? 0;
|
||||
if ($value >= 1) {
|
||||
if ($value !== 0) {
|
||||
$storageTags = $tags . ",bucketId=" . ($this->params['bucketId'] ?? '');
|
||||
$this->statsd->increment($metric . $storageTags);
|
||||
$this->statsd->count($metric . $storageTags, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,19 +186,30 @@ class Stats
|
||||
$functionBuildTime = ($this->params['buildTime'] ?? 0) * 1000; // ms
|
||||
$functionBuildStatus = $this->params['buildStatus'] ?? '';
|
||||
$functionCompute = $functionExecutionTime + $functionBuildTime;
|
||||
$functionTags = $tags . ',functionId=' . $functionId;
|
||||
|
||||
$deploymentSize = $this->params['deployment.{scope}.storage.size'] ?? 0;
|
||||
$storageSize = $this->params['files.{scope}.storage.size'] ?? 0;
|
||||
if ($deploymentSize + $storageSize > 0 || $deploymentSize + $storageSize <= -1) {
|
||||
$this->statsd->count('project.{scope}.storage.size' . $tags, $deploymentSize + $storageSize);
|
||||
}
|
||||
|
||||
if ($deploymentSize !== 0) {
|
||||
$this->statsd->count('deployments.{scope}.storage.size' . $functionTags, $deploymentSize);
|
||||
}
|
||||
|
||||
if ($functionExecution >= 1) {
|
||||
$this->statsd->increment('executions.{scope}.compute' . $tags . ',functionId=' . $functionId . ',functionStatus=' . $functionExecutionStatus);
|
||||
$this->statsd->increment('executions.{scope}.compute' . $functionTags . ',functionStatus=' . $functionExecutionStatus);
|
||||
if ($functionExecutionTime > 0) {
|
||||
$this->statsd->count('executions.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
|
||||
$this->statsd->count('executions.{scope}.compute.time' . $functionTags, $functionExecutionTime);
|
||||
}
|
||||
}
|
||||
if ($functionBuild >= 1) {
|
||||
$this->statsd->increment('builds.{scope}.compute' . $tags . ',functionId=' . $functionId . ',functionBuildStatus=' . $functionBuildStatus);
|
||||
$this->statsd->count('builds.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionBuildTime);
|
||||
$this->statsd->increment('builds.{scope}.compute' . $functionTags . ',functionBuildStatus=' . $functionBuildStatus);
|
||||
$this->statsd->count('builds.{scope}.compute.time' . $functionTags, $functionBuildTime);
|
||||
}
|
||||
if ($functionBuild + $functionExecution >= 1) {
|
||||
$this->statsd->count('project.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionCompute);
|
||||
$this->statsd->count('project.{scope}.compute.time' . $functionTags, $functionCompute);
|
||||
}
|
||||
|
||||
$this->reset();
|
||||
|
||||
@@ -10,26 +10,26 @@ use Utopia\Database\Query;
|
||||
class IndexedQueries extends Queries
|
||||
{
|
||||
/**
|
||||
* @var Document[]
|
||||
* @var array<Document>
|
||||
*/
|
||||
protected $attributes = [];
|
||||
protected array $attributes = [];
|
||||
|
||||
/**
|
||||
* @var Document[]
|
||||
* @var array<Document>
|
||||
*/
|
||||
protected $indexes = [];
|
||||
protected array $indexes = [];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
* This Queries Validator filters indexes for only available indexes
|
||||
*
|
||||
* @param Document[] $attributes
|
||||
* @param Document[] $indexes
|
||||
* @param array<Document> $attributes
|
||||
* @param array<Document> $indexes
|
||||
* @param Base ...$validators
|
||||
* @param bool $strict
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($attributes = [], $indexes = [], Base ...$validators)
|
||||
public function __construct(array $attributes = [], array $indexes = [], Base ...$validators)
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
|
||||
@@ -55,33 +55,6 @@ class IndexedQueries extends Queries
|
||||
parent::__construct(...$validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if indexed array $indexes matches $queries
|
||||
*
|
||||
* @param array $indexes
|
||||
* @param array $queries
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function arrayMatch(array $indexes, array $queries): bool
|
||||
{
|
||||
// Check the count of indexes first for performance
|
||||
if (count($queries) !== count($indexes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sort them for comparison, the order is not important here anymore.
|
||||
sort($indexes, SORT_STRING);
|
||||
sort($queries, SORT_STRING);
|
||||
|
||||
// Only matching arrays will have equal diffs in both directions
|
||||
if (array_diff_assoc($indexes, $queries) !== array_diff_assoc($queries, $indexes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
@@ -111,41 +84,26 @@ class IndexedQueries extends Queries
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
/** @var Query[] */ $filters = $grouped['filters'];
|
||||
/** @var string[] */ $orderAttributes = $grouped['orderAttributes'];
|
||||
$filters = $grouped['filters'];
|
||||
|
||||
// Check filter queries for exact index match
|
||||
if (count($filters) > 0) {
|
||||
$filtersByAttribute = [];
|
||||
foreach ($filters as $filter) {
|
||||
$filtersByAttribute[$filter->getAttribute()] = $filter->getMethod();
|
||||
}
|
||||
foreach ($filters as $filter) {
|
||||
if ($filter->getMethod() === Query::TYPE_SEARCH) {
|
||||
$matched = false;
|
||||
|
||||
$found = null;
|
||||
foreach ($this->indexes as $index) {
|
||||
if (
|
||||
$index->getAttribute('type') === Database::INDEX_FULLTEXT
|
||||
&& $index->getAttribute('attributes') === [$filter->getAttribute()]
|
||||
) {
|
||||
$matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->indexes as $index) {
|
||||
if ($this->arrayMatch($index->getAttribute('attributes'), array_keys($filtersByAttribute))) {
|
||||
$found = $index;
|
||||
if (!$matched) {
|
||||
$this->message = "Searching by attribute \"{$filter->getAttribute()}\" requires a fulltext index.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute));
|
||||
return false;
|
||||
}
|
||||
|
||||
// search method requires fulltext index
|
||||
if (in_array(Query::TYPE_SEARCH, array_values($filtersByAttribute)) && $found['type'] !== Database::INDEX_FULLTEXT) {
|
||||
$this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($filtersByAttribute));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check order attributes for exact index match
|
||||
$validator = new OrderAttributes($this->attributes, $this->indexes, true);
|
||||
if (count($orderAttributes) > 0 && !$validator->isValid($orderAttributes)) {
|
||||
$this->message = $validator->getDescription();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -11,12 +11,12 @@ class Queries extends Validator
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Invalid queries';
|
||||
protected string $message = 'Invalid queries';
|
||||
|
||||
/**
|
||||
* @var Base[]
|
||||
* @var array<Base>
|
||||
*/
|
||||
protected $validators;
|
||||
protected array $validators;
|
||||
|
||||
/**
|
||||
* Queries constructor
|
||||
@@ -57,41 +57,35 @@ class Queries extends Validator
|
||||
if (!$query instanceof Query) {
|
||||
try {
|
||||
$query = Query::parse($query);
|
||||
} catch (\Throwable $th) {
|
||||
$this->message = 'Invalid query: ${query}';
|
||||
} catch (\Throwable) {
|
||||
$this->message = "Invalid query: {$query}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$method = $query->getMethod();
|
||||
$methodType = '';
|
||||
switch ($method) {
|
||||
case Query::TYPE_LIMIT:
|
||||
$methodType = Base::METHOD_TYPE_LIMIT;
|
||||
break;
|
||||
case Query::TYPE_OFFSET:
|
||||
$methodType = Base::METHOD_TYPE_OFFSET;
|
||||
break;
|
||||
case Query::TYPE_CURSORAFTER:
|
||||
case Query::TYPE_CURSORBEFORE:
|
||||
$methodType = Base::METHOD_TYPE_CURSOR;
|
||||
break;
|
||||
case Query::TYPE_ORDERASC:
|
||||
case Query::TYPE_ORDERDESC:
|
||||
$methodType = Base::METHOD_TYPE_ORDER;
|
||||
break;
|
||||
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:
|
||||
$methodType = Base::METHOD_TYPE_FILTER;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
$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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -19,6 +20,7 @@ class Base extends Queries
|
||||
*
|
||||
* @param string $collection
|
||||
* @param string[] $allowedAttributes
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(string $collection, array $allowedAttributes)
|
||||
{
|
||||
@@ -65,6 +67,7 @@ class Base extends Queries
|
||||
new Cursor(),
|
||||
new Filter($attributes),
|
||||
new Order($attributes),
|
||||
new Select($attributes),
|
||||
];
|
||||
|
||||
parent::__construct(...$validators);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
use Appwrite\Utopia\Database\Validator\IndexedQueries;
|
||||
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\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;
|
||||
|
||||
@@ -17,7 +18,7 @@ class Documents extends IndexedQueries
|
||||
* Expression constructor
|
||||
*
|
||||
* @param Document[] $attributes
|
||||
* @param Document[] $indexes
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(array $attributes, array $indexes)
|
||||
{
|
||||
@@ -43,6 +44,7 @@ class Documents extends IndexedQueries
|
||||
new Cursor(),
|
||||
new Filter($attributes),
|
||||
new Order($attributes),
|
||||
new Select($attributes),
|
||||
];
|
||||
|
||||
parent::__construct($attributes, $indexes, ...$validators);
|
||||
|
||||
@@ -7,7 +7,8 @@ use Appwrite\Utopia\Database\Validator\Queries\Base;
|
||||
class Projects extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'name'
|
||||
'name',
|
||||
'teamId'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ abstract class Base extends Validator
|
||||
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
|
||||
|
||||
@@ -18,6 +18,8 @@ class Filter extends Base
|
||||
*/
|
||||
protected $schema = [];
|
||||
|
||||
private int $maxValuesCount;
|
||||
|
||||
/**
|
||||
* Query constructor
|
||||
*
|
||||
@@ -34,6 +36,18 @@ class Filter extends Base
|
||||
|
||||
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;
|
||||
@@ -49,6 +63,12 @@ class Filter extends Base
|
||||
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) {
|
||||
@@ -61,7 +81,9 @@ class Filter extends Base
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
@@ -99,6 +121,11 @@ class Filter extends Base
|
||||
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);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class Limit extends Base
|
||||
*
|
||||
* @param int $maxLimit
|
||||
*/
|
||||
public function __construct(int $maxLimit = 100)
|
||||
public function __construct(int $maxLimit = PHP_INT_MAX)
|
||||
{
|
||||
$this->maxLimit = $maxLimit;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Query;
|
||||
|
||||
use Appwrite\Utopia\Database\Validator\Query\Base;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
@@ -15,7 +14,7 @@ class Offset extends Base
|
||||
*
|
||||
* @param int $maxOffset
|
||||
*/
|
||||
public function __construct(int $maxOffset = 5000)
|
||||
public function __construct(int $maxOffset = PHP_INT_MAX)
|
||||
{
|
||||
$this->maxOffset = $maxOffset;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -9,66 +9,37 @@ use Utopia\Swoole\Request as UtopiaRequest;
|
||||
|
||||
class Request extends UtopiaRequest
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
private static $filter = null;
|
||||
private static ?Filter $filter = null;
|
||||
private static ?Route $route = null;
|
||||
|
||||
/**
|
||||
* @var Route
|
||||
*/
|
||||
private static $route = null;
|
||||
|
||||
/**
|
||||
* Request constructor.
|
||||
*/
|
||||
public function __construct(SwooleRequest $request)
|
||||
{
|
||||
parent::__construct($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Params
|
||||
*
|
||||
* Get all params of current method
|
||||
*
|
||||
* @return array
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
$requestParameters = [];
|
||||
|
||||
switch ($this->getMethod()) {
|
||||
case self::METHOD_GET:
|
||||
$requestParameters = (!empty($this->swoole->get)) ? $this->swoole->get : [];
|
||||
break;
|
||||
case self::METHOD_POST:
|
||||
case self::METHOD_PUT:
|
||||
case self::METHOD_PATCH:
|
||||
case self::METHOD_DELETE:
|
||||
$requestParameters = $this->generateInput();
|
||||
break;
|
||||
default:
|
||||
$requestParameters = (!empty($this->swoole->get)) ? $this->swoole->get : [];
|
||||
}
|
||||
$parameters = parent::getParams();
|
||||
|
||||
if (self::hasFilter() && self::hasRoute()) {
|
||||
$endpointIdentifier = self::getRoute()->getLabel('sdk.namespace', 'unknown') . '.' . self::getRoute()->getLabel('sdk.method', 'unknown');
|
||||
$requestParameters = self::getFilter()->parse($requestParameters, $endpointIdentifier);
|
||||
$parameters = self::getFilter()->parse($parameters, $endpointIdentifier);
|
||||
}
|
||||
|
||||
return $requestParameters;
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Function to set a response filter
|
||||
*
|
||||
* @param $filter the response filter to set
|
||||
* @param Filter|null $filter Filter the response filter to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setFilter(?Filter $filter)
|
||||
public static function setFilter(?Filter $filter): void
|
||||
{
|
||||
self::$filter = $filter;
|
||||
}
|
||||
@@ -76,7 +47,7 @@ class Request extends UtopiaRequest
|
||||
/**
|
||||
* Return the currently set filter
|
||||
*
|
||||
* @return Filter
|
||||
* @return Filter|null
|
||||
*/
|
||||
public static function getFilter(): ?Filter
|
||||
{
|
||||
@@ -96,19 +67,19 @@ class Request extends UtopiaRequest
|
||||
/**
|
||||
* Function to set a request route
|
||||
*
|
||||
* @param Route $route the request route to set
|
||||
* @param Route|null $route the request route to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setRoute(?Route $route)
|
||||
public static function setRoute(?Route $route): void
|
||||
{
|
||||
self::$route = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently get route
|
||||
* Return the current route
|
||||
*
|
||||
* @return Route
|
||||
* @return Route|null
|
||||
*/
|
||||
public static function getRoute(): ?Route
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Permission;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class V15 extends Filter
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Utopia;
|
||||
|
||||
use Exception;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Swoole\Http\Response as SwooleHTTPResponse;
|
||||
use Utopia\Database\Document;
|
||||
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeEnum;
|
||||
use Appwrite\Utopia\Response\Model\AttributeIP;
|
||||
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\Collection;
|
||||
use Appwrite\Utopia\Response\Model\Database;
|
||||
@@ -43,6 +45,7 @@ use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Appwrite\Utopia\Response\Model\Build;
|
||||
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\Index;
|
||||
use Appwrite\Utopia\Response\Model\JWT;
|
||||
@@ -70,6 +73,7 @@ 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\Provider;
|
||||
use Appwrite\Utopia\Response\Model\Runtime;
|
||||
use Appwrite\Utopia\Response\Model\UsageBuckets;
|
||||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
@@ -83,6 +87,7 @@ use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\Variable;
|
||||
|
||||
/**
|
||||
* @method int getStatusCode()
|
||||
* @method Response setStatusCode(int $code = 200)
|
||||
*/
|
||||
class Response extends SwooleResponse
|
||||
@@ -129,6 +134,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_ATTRIBUTE_IP = 'attributeIp';
|
||||
public const MODEL_ATTRIBUTE_URL = 'attributeUrl';
|
||||
public const MODEL_ATTRIBUTE_DATETIME = 'attributeDatetime';
|
||||
public const MODEL_ATTRIBUTE_RELATIONSHIP = 'attributeRelationship';
|
||||
|
||||
// Users
|
||||
public const MODEL_ACCOUNT = 'account';
|
||||
@@ -194,6 +200,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_WEBHOOK_LIST = 'webhookList';
|
||||
public const MODEL_KEY = 'key';
|
||||
public const MODEL_KEY_LIST = 'keyList';
|
||||
public const MODEL_PROVIDER = 'provider';
|
||||
public const MODEL_PROVIDER_LIST = 'providerList';
|
||||
public const MODEL_PLATFORM = 'platform';
|
||||
public const MODEL_PLATFORM_LIST = 'platformList';
|
||||
public const MODEL_DOMAIN = 'domain';
|
||||
@@ -208,6 +216,9 @@ class Response extends SwooleResponse
|
||||
public const MODEL_HEALTH_TIME = 'healthTime';
|
||||
public const MODEL_HEALTH_ANTIVIRUS = 'healthAntivirus';
|
||||
|
||||
// Console
|
||||
public const MODEL_CONSOLE_VARIABLES = 'consoleVariables';
|
||||
|
||||
// Deprecated
|
||||
public const MODEL_PERMISSIONS = 'permissions';
|
||||
public const MODEL_RULE = 'rule';
|
||||
@@ -259,6 +270,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
|
||||
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
|
||||
->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))
|
||||
@@ -282,6 +294,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new AttributeIP())
|
||||
->setModel(new AttributeURL())
|
||||
->setModel(new AttributeDatetime())
|
||||
->setModel(new AttributeRelationship())
|
||||
->setModel(new Index())
|
||||
->setModel(new ModelDocument())
|
||||
->setModel(new Log())
|
||||
@@ -312,6 +325,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new Webhook())
|
||||
->setModel(new Key())
|
||||
->setModel(new Domain())
|
||||
->setModel(new Provider())
|
||||
->setModel(new Platform())
|
||||
->setModel(new Variable())
|
||||
->setModel(new Country())
|
||||
@@ -334,6 +348,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new UsageFunctions())
|
||||
->setModel(new UsageFunction())
|
||||
->setModel(new UsageProject())
|
||||
->setModel(new ConsoleVariables())
|
||||
// Verification
|
||||
// Recovery
|
||||
// Tests (keep last)
|
||||
@@ -346,6 +361,7 @@ class Response extends SwooleResponse
|
||||
* HTTP content types
|
||||
*/
|
||||
public const CONTENT_TYPE_YAML = 'application/x-yaml';
|
||||
public const CONTENT_TYPE_NULL = 'null';
|
||||
|
||||
/**
|
||||
* List of defined output objects
|
||||
@@ -367,7 +383,9 @@ class Response extends SwooleResponse
|
||||
/**
|
||||
* Get Model Object
|
||||
*
|
||||
* @param string $key
|
||||
* @return Model
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getModel(string $key): Model
|
||||
{
|
||||
@@ -396,6 +414,7 @@ class Response extends SwooleResponse
|
||||
* @param string $model
|
||||
*
|
||||
* return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function dynamic(Document $document, string $model): void
|
||||
{
|
||||
@@ -406,7 +425,26 @@ class Response extends SwooleResponse
|
||||
$output = self::getFilter()->parse($output, $model);
|
||||
}
|
||||
|
||||
$this->json(!empty($output) ? $output : new \stdClass());
|
||||
switch ($this->getContentType()) {
|
||||
case self::CONTENT_TYPE_JSON:
|
||||
$this->json(!empty($output) ? $output : new \stdClass());
|
||||
break;
|
||||
|
||||
case self::CONTENT_TYPE_YAML:
|
||||
$this->yaml(!empty($output) ? $output : new \stdClass());
|
||||
break;
|
||||
|
||||
case self::CONTENT_TYPE_NULL:
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($model === self::MODEL_NONE) {
|
||||
$this->noContent();
|
||||
} else {
|
||||
$this->json(!empty($output) ? $output : new \stdClass());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,6 +454,8 @@ class Response extends SwooleResponse
|
||||
* @param string $model
|
||||
*
|
||||
* return array
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function output(Document $document, string $model): array
|
||||
{
|
||||
@@ -515,6 +555,7 @@ class Response extends SwooleResponse
|
||||
* @param array $data
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function yaml(array $data): void
|
||||
{
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace Appwrite\Utopia\Response\Filters;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Permission;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class V15 extends Filter
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ abstract class Model
|
||||
public const TYPE_JSON = 'json';
|
||||
public const TYPE_DATETIME = 'datetime';
|
||||
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00';
|
||||
public const TYPE_RELATIONSHIP = 'relationship';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
@@ -80,6 +81,7 @@ abstract class Model
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $options
|
||||
* @return Model
|
||||
*/
|
||||
protected function addRule(string $key, array $options): self
|
||||
{
|
||||
@@ -98,7 +100,7 @@ abstract class Model
|
||||
* If rule exists, it will be removed
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $options
|
||||
* @return Model
|
||||
*/
|
||||
protected function removeRule(string $key): self
|
||||
{
|
||||
@@ -109,7 +111,10 @@ abstract class Model
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequired()
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRequired(): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ class AlgoArgon2 extends Model
|
||||
{
|
||||
// No options if imported. If hashed by Appwrite, following configuration is available:
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'argon2',
|
||||
'example' => 'argon2',
|
||||
])
|
||||
->addRule('memoryCost', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Memory used to compute hash.',
|
||||
|
||||
@@ -10,6 +10,13 @@ class AlgoBcrypt extends Model
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'bcrypt',
|
||||
'example' => 'bcrypt',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,14 @@ class AlgoMd5 extends Model
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'md5',
|
||||
'example' => 'md5',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,14 @@ class AlgoPhpass extends Model
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'phpass',
|
||||
'example' => 'phpass',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,12 @@ class AlgoScrypt extends Model
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'scrypt',
|
||||
'example' => 'scrypt',
|
||||
])
|
||||
->addRule('costCpu', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'CPU complexity of computed hash.',
|
||||
|
||||
@@ -10,6 +10,12 @@ class AlgoScryptModified extends Model
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'scryptMod',
|
||||
'example' => 'scryptMod',
|
||||
])
|
||||
->addRule('salt', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Salt used to compute hash.',
|
||||
|
||||
@@ -10,6 +10,14 @@ class AlgoSha extends Model
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Algo type.',
|
||||
'default' => 'sha',
|
||||
'example' => 'sha',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeBoolean extends Attribute
|
||||
{
|
||||
|
||||
@@ -18,10 +18,10 @@ class AttributeDatetime extends Attribute
|
||||
'example' => 'birthDay',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Attribute type.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
'example' => self::TYPE_DATETIME,
|
||||
])
|
||||
->addRule('format', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeEmail extends Attribute
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeEnum extends Attribute
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeFloat extends Attribute
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeIP extends Attribute
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeInteger extends Attribute
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class AttributeList extends Model
|
||||
{
|
||||
@@ -27,6 +26,7 @@ class AttributeList extends Model
|
||||
Response::MODEL_ATTRIBUTE_URL,
|
||||
Response::MODEL_ATTRIBUTE_IP,
|
||||
Response::MODEL_ATTRIBUTE_DATETIME,
|
||||
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
|
||||
Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute
|
||||
],
|
||||
'description' => 'List of attributes.',
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class AttributeRelationship extends Attribute
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('relatedCollection', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The ID of the related collection.',
|
||||
'default' => null,
|
||||
'example' => 'collection',
|
||||
])
|
||||
->addRule('relationType', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The type of the relationship.',
|
||||
'default' => '',
|
||||
'example' => 'oneToOne|oneToMany|manyToOne|manyToMany',
|
||||
])
|
||||
->addRule('twoWay', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Is the relationship two-way?',
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('twoWayKey', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The key of the two-way relationship.',
|
||||
'default' => '',
|
||||
'example' => 'string',
|
||||
])
|
||||
->addRule('onDelete', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'How deleting the parent document will propagate to child documents.',
|
||||
'default' => 'restrict',
|
||||
'example' => 'restrict|cascade|setNull',
|
||||
])
|
||||
->addRule('side', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Whether this is the parent or child side of the relationship',
|
||||
'default' => '',
|
||||
'example' => 'parent|child',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public array $conditions = [
|
||||
'type' => self::TYPE_RELATIONSHIP,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'AttributeRelationship';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_ATTRIBUTE_RELATIONSHIP;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeString extends Attribute
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
|
||||
class AttributeURL extends Attribute
|
||||
{
|
||||
|
||||
@@ -69,6 +69,7 @@ class Collection extends Model
|
||||
Response::MODEL_ATTRIBUTE_URL,
|
||||
Response::MODEL_ATTRIBUTE_IP,
|
||||
Response::MODEL_ATTRIBUTE_DATETIME,
|
||||
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
|
||||
Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute
|
||||
],
|
||||
'description' => 'Collection attributes.',
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class ConsoleVariables extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('_APP_DOMAIN_TARGET', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'CNAME target for your Appwrite custom domains.',
|
||||
'default' => '',
|
||||
'example' => 'appwrite.io',
|
||||
])
|
||||
->addRule('_APP_STORAGE_LIMIT', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Maximum file size allowed for file upload in bytes.',
|
||||
'default' => '',
|
||||
'example' => '30000000',
|
||||
])
|
||||
->addRule('_APP_FUNCTIONS_SIZE_LIMIT', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Maximum file size allowed for deployment in bytes.',
|
||||
'default' => '',
|
||||
'example' => '30000000',
|
||||
])
|
||||
->addRule('_APP_USAGE_STATS', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'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',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Console Variables';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_CONSOLE_VARIABLES;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@ class Database extends Model
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('enabled', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Database enabled.',
|
||||
'default' => true,
|
||||
'example' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ class Deployment extends Model
|
||||
'default' => '',
|
||||
'example' => 'enabled',
|
||||
])
|
||||
->addRule('buildTime', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'The current build time in seconds.',
|
||||
'default' => 0,
|
||||
'example' => 128,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,18 @@ class Document extends Any
|
||||
$document->removeAttribute('$internalId');
|
||||
$document->removeAttribute('$collection'); // $collection is the internal collection ID
|
||||
|
||||
foreach ($document->getAttributes() as $attribute) {
|
||||
if (\is_array($attribute)) {
|
||||
foreach ($attribute as $subAttribute) {
|
||||
if ($subAttribute instanceof DatabaseDocument) {
|
||||
$this->filter($subAttribute);
|
||||
}
|
||||
}
|
||||
} elseif ($attribute instanceof DatabaseDocument) {
|
||||
$this->filter($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Role;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class Execution extends Model
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ class Func extends Model
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Function execution timeout in seconds.',
|
||||
'default' => 15,
|
||||
'example' => 1592981237,
|
||||
'example' => 15,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class Locale extends Model
|
||||
])
|
||||
->addRule('eu', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'True if country is part of the Europian Union.',
|
||||
'description' => 'True if country is part of the European Union.',
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
|
||||
@@ -41,9 +41,9 @@ class Platform extends Model
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform type. Possible values are: web, flutter-ios, flutter-android, ios, android, and unity.',
|
||||
'description' => 'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, ios, android, and unity.',
|
||||
'default' => '',
|
||||
'example' => 'My Web App',
|
||||
'example' => 'web',
|
||||
])
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Config\Config;
|
||||
@@ -101,12 +102,43 @@ class Project extends Model
|
||||
'default' => '',
|
||||
'example' => '131102020',
|
||||
])
|
||||
->addRule('authDuration', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Session duration in seconds.',
|
||||
'default' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'example' => 60,
|
||||
])
|
||||
->addRule('authLimit', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Max users allowed. 0 is unlimited.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('authSessionsLimit', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Max sessions allowed per user. 100 maximum.',
|
||||
'default' => 10,
|
||||
'example' => 10,
|
||||
])
|
||||
->addRule('authPasswordHistory', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Max allowed passwords in the history list per user. Max passwords limit allowed in history is 20. Use 0 for disabling password history.',
|
||||
'default' => 0,
|
||||
'example' => 5,
|
||||
])
|
||||
->addRule('authPasswordDictionary', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('providers', [
|
||||
'type' => Response::MODEL_PROVIDER,
|
||||
'description' => 'List of Providers.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('platforms', [
|
||||
'type' => Response::MODEL_PLATFORM,
|
||||
'description' => 'List of Platforms.',
|
||||
@@ -138,32 +170,8 @@ class Project extends Model
|
||||
;
|
||||
|
||||
$services = Config::getParam('services', []);
|
||||
$providers = Config::getParam('providers', []);
|
||||
$auth = Config::getParam('auth', []);
|
||||
|
||||
foreach ($providers as $index => $provider) {
|
||||
if (!$provider['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = (isset($provider['name'])) ? $provider['name'] : 'Unknown';
|
||||
|
||||
$this
|
||||
->addRule('provider' . \ucfirst($index) . 'Appid', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => $name . ' OAuth app ID.',
|
||||
'example' => '123247283472834787438',
|
||||
'default' => '',
|
||||
])
|
||||
->addRule('provider' . \ucfirst($index) . 'Secret', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => $name . ' OAuth secret ID.',
|
||||
'example' => 'djsgudsdsewe43434343dd34...',
|
||||
'default' => '',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
foreach ($auth as $index => $method) {
|
||||
$name = $method['name'] ?? '';
|
||||
$key = $method['key'] ?? '';
|
||||
@@ -224,6 +232,7 @@ class Project extends Model
|
||||
*/
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
// Services
|
||||
$values = $document->getAttribute('services', []);
|
||||
$services = Config::getParam('services', []);
|
||||
|
||||
@@ -236,10 +245,15 @@ class Project extends Model
|
||||
$document->setAttribute('serviceStatusFor' . ucfirst($key), $value);
|
||||
}
|
||||
|
||||
// Auth
|
||||
$authValues = $document->getAttribute('auths', []);
|
||||
$auth = Config::getParam('auth', []);
|
||||
|
||||
$document->setAttribute('authLimit', $authValues['limit'] ?? 0);
|
||||
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
|
||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||
|
||||
foreach ($auth as $index => $method) {
|
||||
$key = $method['key'];
|
||||
@@ -247,17 +261,27 @@ class Project extends Model
|
||||
$document->setAttribute('auth' . ucfirst($key), $value);
|
||||
}
|
||||
|
||||
// Providers
|
||||
$providers = Config::getParam('providers', []);
|
||||
$providerValues = $document->getAttribute('authProviders', []);
|
||||
$projectProviders = [];
|
||||
|
||||
foreach ($providers as $key => $provider) {
|
||||
if (!$provider['enabled']) {
|
||||
// Disabled by Appwrite configuration, exclude from response
|
||||
continue;
|
||||
}
|
||||
$appId = $providerValues[$key . 'Appid'] ?? '';
|
||||
$secret = $providerValues[$key . 'Secret'] ?? '';
|
||||
$document->setAttribute('provider' . ucfirst($key) . 'Appid', $appId)->setAttribute('provider' . ucfirst($key) . 'Secret', $secret);
|
||||
|
||||
$projectProviders[] = new Document([
|
||||
'name' => ucfirst($key),
|
||||
'appId' => $providerValues[$key . 'Appid'] ?? '',
|
||||
'secret' => $providerValues[$key . 'Secret'] ?? '',
|
||||
'enabled' => $providerValues[$key . 'Enabled'] ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
$document->setAttribute("providers", $projectProviders);
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Provider extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider name.',
|
||||
'default' => '',
|
||||
'example' => 'GitHub',
|
||||
])
|
||||
->addRule('appId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'OAuth 2.0 application ID.',
|
||||
'default' => '',
|
||||
'example' => '259125845563242502',
|
||||
])
|
||||
->addRule('secret', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
|
||||
'default' => '',
|
||||
'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
|
||||
])
|
||||
->addRule('enabled', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Provider is active and can be used to create session.',
|
||||
'example' => '',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Provider';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PROVIDER;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Team extends Model
|
||||
{
|
||||
@@ -40,9 +41,33 @@ class Team extends Model
|
||||
'default' => 0,
|
||||
'example' => 7,
|
||||
])
|
||||
->addRule('prefs', [
|
||||
'type' => Response::MODEL_PREFERENCES,
|
||||
'description' => 'Team preferences as a key-value object',
|
||||
'default' => new \stdClass(),
|
||||
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Document before returning it to the client
|
||||
*
|
||||
* @return Document
|
||||
*/
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
$prefs = $document->getAttribute('prefs');
|
||||
if ($prefs instanceof Document) {
|
||||
$prefs = $prefs->getArrayCopy();
|
||||
}
|
||||
|
||||
if (is_array($prefs) && empty($prefs)) {
|
||||
$document->setAttribute('prefs', new \stdClass());
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
|
||||
@@ -17,45 +17,45 @@ class UsageBuckets extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('filesCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of files in this bucket.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesStorage', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total storage of files in this bucket.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,38 +17,38 @@ class UsageCollection extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('documentsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,73 +17,73 @@ class UsageDatabase extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('documentsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of collections.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections delete.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,108 +17,108 @@ class UsageDatabases extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('databasesCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of collections.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of collections.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for documents deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for collections delete.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,59 +17,59 @@ class UsageFunction extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('executionsTotal', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of function executions.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsFailure', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution failures.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsSuccess', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution successes.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsTotal', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of function builds.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsFailure', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build failures.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsSuccess', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build successes.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,59 +17,59 @@ class UsageFunctions extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('executionsTotal', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of function executions.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsFailure', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution failures.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsSuccess', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution successes.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executionsTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function execution duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsTotal', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of function builds.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsFailure', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build failures.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsSuccess', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build successes.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buildsTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function build duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,52 +17,59 @@ class UsageProject extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('requests', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of requests.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('network', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for consumed bandwidth.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('executions', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for function executions.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documents', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collections', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for number of collections.',
|
||||
->addRule('databases', [
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of databases.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('users', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of users.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('storage', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('buckets', [
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for number of buckets.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
@@ -17,80 +17,80 @@ class UsageStorage extends Model
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('storage', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of files.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('bucketsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for total number of buckets.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('bucketsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for buckets created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('bucketsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for buckets read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('bucketsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for buckets updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('bucketsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for buckets deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('filesDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'type' => Response::MODEL_METRIC,
|
||||
'description' => 'Aggregated stats for files deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user