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:
Jake Barnby
2023-06-15 12:26:29 +12:00
8051 changed files with 154505 additions and 57669 deletions
+5 -5
View File
@@ -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();
}
@@ -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;
}
}
+1 -1
View File
@@ -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;
+5 -19
View File
@@ -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
View File
@@ -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())
]);
}
+12
View File
@@ -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 = '';
+19
View File
@@ -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';
}
}
+88
View File
@@ -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);
}
}
+323
View File
@@ -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;
}
}
+269
View File
@@ -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;
}
}
+57
View File
@@ -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;
}
}
+42
View File
@@ -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);
}
}
+29
View File
@@ -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);
}
}
+53
View File
@@ -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;
}
}
}
+439
View File
@@ -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');
}
}
+43
View File
@@ -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;
}
}
+4 -4
View File
@@ -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()];
}
+52 -2
View File
@@ -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.
*/
+3 -8
View File
@@ -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>
*/
+150
View File
@@ -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;
}
}
+115
View File
@@ -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;
}
}
+223
View File
@@ -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());
}
}
}
+4
View File
@@ -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;
-84
View File
@@ -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;
}
}
-114
View File
@@ -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;
}
}
+11 -2
View File
@@ -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;
-92
View File
@@ -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;
}
}
+194
View File
@@ -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;
}
}
+70
View File
@@ -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);
});
}
}
+64 -40
View File
@@ -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);
-75
View File
@@ -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;
}
}
-38
View File
@@ -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}",
]
);
}
}
-46
View File
@@ -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}",
]
);
}
}
-39
View File
@@ -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}"
);
}
}
-42
View File
@@ -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}",
]
);
}
}
-36
View File
@@ -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}"
);
}
}
-41
View File
@@ -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
])
);
}
}
+19 -4
View File
@@ -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);
+25 -5
View File
@@ -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'];
}
+7
View File
@@ -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);
});
}
}
-360
View File
@@ -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);
});
}
}
+143 -35
View File
@@ -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;
}
}
}
}
+35 -14
View File
@@ -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;
}
}
+13 -42
View File
@@ -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
{
+2 -2
View File
@@ -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
{
+42 -1
View File
@@ -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
{
+2 -2
View File
@@ -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
{
+7 -2
View File
@@ -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
{
+1 -1
View File
@@ -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,
+51 -27
View File
@@ -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