mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch 'master' into feat-password-hash-algos
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
|
||||
class Autodesk extends OAuth2
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'user-profile:read',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'autodesk';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
return 'https://developer.api.autodesk.com/authentication/v1/authorize?' . \http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'state' => \json_encode($this->state),
|
||||
'redirect_uri' => $this->callback,
|
||||
'response_type' => 'code'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$headers = ['Content-Type: application/x-www-form-urlencoded'];
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
'https://developer.api.autodesk.com/authentication/v1/gettoken',
|
||||
$headers,
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'client_secret' => $this->appSecret,
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code'
|
||||
])
|
||||
);
|
||||
|
||||
$this->tokens = \json_decode($response, true);
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
'https://developer.api.autodesk.com/authentication/v1/refreshtoken',
|
||||
[],
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $this->callback,
|
||||
])
|
||||
);
|
||||
|
||||
$output = [];
|
||||
\parse_str($response, $output);
|
||||
$this->tokens = $output;
|
||||
|
||||
if (empty($this->tokens['refresh_token'])) {
|
||||
$this->tokens['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserID(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['userId'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['emailId'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the OAuth email is verified
|
||||
*
|
||||
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if ($user['emailVerified'] ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['userName'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$headers = ['Authorization: Bearer ' . \urlencode($accessToken)];
|
||||
$user = $this->request('GET', 'https://developer.api.autodesk.com/userprofile/v1/users/@me', $headers);
|
||||
$this->user = \json_decode($user, true);
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class Gitlab extends OAuth2
|
||||
*/
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
return 'https://gitlab.com/oauth/authorize?' . \http_build_query([
|
||||
return $this->getEndpoint() . '/oauth/authorize?' . \http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
@@ -58,10 +58,10 @@ class Gitlab extends OAuth2
|
||||
if (empty($this->tokens)) {
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
'https://gitlab.com/oauth/token?' . \http_build_query([
|
||||
$this->getEndpoint() . '/oauth/token?' . \http_build_query([
|
||||
'code' => $code,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'client_secret' => $this->getAppSecret()['clientSecret'],
|
||||
'redirect_uri' => $this->callback,
|
||||
'grant_type' => 'authorization_code'
|
||||
])
|
||||
@@ -80,10 +80,10 @@ class Gitlab extends OAuth2
|
||||
{
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
'https://gitlab.com/oauth/token?' . \http_build_query([
|
||||
$this->getEndpoint() . '/oauth/token?' . \http_build_query([
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'client_secret' => $this->getAppSecret()['clientSecret'],
|
||||
'grant_type' => 'refresh_token'
|
||||
])
|
||||
), true);
|
||||
@@ -163,10 +163,39 @@ class Gitlab extends OAuth2
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$user = $this->request('GET', 'https://gitlab.com/api/v4/user?access_token=' . \urlencode($accessToken));
|
||||
$user = $this->request('GET', $this->getEndpoint() . '/api/v4/user?access_token=' . \urlencode($accessToken));
|
||||
$this->user = \json_decode($user, true);
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON stored in appSecret
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAppSecret(): array
|
||||
{
|
||||
try {
|
||||
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Throwable $th) {
|
||||
throw new \Exception('Invalid secret');
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEndpoint(): string
|
||||
{
|
||||
$defaultEndpoint = 'https://gitlab.com';
|
||||
$secret = $this->getAppSecret();
|
||||
$endpoint = $secret['endpoint'] ?? $defaultEndpoint;
|
||||
return empty($endpoint) ? $defaultEndpoint : $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://docs.msg91.com/p/tf9GTextN/e/Irz7-x1PK/MSG91
|
||||
|
||||
class Msg91 extends Phone
|
||||
{
|
||||
/**
|
||||
* @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_PHONE_PROVIDER format is 'phone://[senderID]:[authKey]@msg91'.
|
||||
* _APP_PHONE_FROM value is flow ID created in Msg91
|
||||
* Eg. _APP_PHONE_PROVIDER = phone://DINESH:5e1e93cad6fc054d8e759a5b@msg91
|
||||
* _APP_PHONE_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}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace Appwrite\Auth\Phone;
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://www.twilio.com/docs/sms/api
|
||||
// https://developer.telesign.com/enterprise/docs/sms-api-send-an-sms
|
||||
|
||||
class Telesign extends Phone
|
||||
{
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://developer.vonage.com/api/sms
|
||||
|
||||
class Vonage extends Phone
|
||||
{
|
||||
/**
|
||||
* @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
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Utopia\Database\Document;
|
||||
class Database extends Event
|
||||
{
|
||||
protected string $type = '';
|
||||
protected ?Document $database = null;
|
||||
protected ?Document $collection = null;
|
||||
protected ?Document $document = null;
|
||||
|
||||
@@ -38,6 +39,18 @@ class Database extends Event
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database for this event
|
||||
*
|
||||
* @param Document $database
|
||||
* @return self
|
||||
*/
|
||||
public function setDatabase(Document $database): self
|
||||
{
|
||||
$this->database = $database;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the collection for this database event.
|
||||
*
|
||||
@@ -97,6 +110,7 @@ class Database extends Event
|
||||
'type' => $this->type,
|
||||
'collection' => $this->collection,
|
||||
'document' => $this->document,
|
||||
'database' => $this->database,
|
||||
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ class Event
|
||||
protected string $event = '';
|
||||
protected array $params = [];
|
||||
protected array $payload = [];
|
||||
protected array $context = [];
|
||||
protected ?Document $project = null;
|
||||
protected ?Document $user = null;
|
||||
protected ?Document $context = null;
|
||||
|
||||
/**
|
||||
* @param string $queue
|
||||
@@ -172,12 +172,13 @@ class Event
|
||||
/**
|
||||
* Set context for this event.
|
||||
*
|
||||
* @param string $key
|
||||
* @param Document $context
|
||||
* @return self
|
||||
*/
|
||||
public function setContext(Document $context): self
|
||||
public function setContext(string $key, Document $context): self
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->context[$key] = $context;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -185,11 +186,13 @@ class Event
|
||||
/**
|
||||
* Get context for this event.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getContext(): ?Document
|
||||
public function getContext(string $key): ?Document
|
||||
{
|
||||
return $this->context;
|
||||
return $this->context[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,14 +298,28 @@ class Event
|
||||
$type = $parts[0] ?? false;
|
||||
$resource = $parts[1] ?? false;
|
||||
$hasSubResource = $count > 3 && \str_starts_with($parts[3], '[');
|
||||
$hasSubSubResource = $count > 5 && \str_starts_with($parts[5], '[') && $hasSubResource;
|
||||
|
||||
if ($hasSubResource) {
|
||||
$subType = $parts[2];
|
||||
$subResource = $parts[3];
|
||||
}
|
||||
|
||||
if ($hasSubSubResource) {
|
||||
$subSubType = $parts[4];
|
||||
$subSubResource = $parts[5];
|
||||
if ($count == 8) {
|
||||
$attribute = $parts[7];
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasSubResource && !$hasSubSubResource) {
|
||||
if ($count === 6) {
|
||||
$attribute = $parts[5];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!$hasSubResource) {
|
||||
if ($count === 4) {
|
||||
$attribute = $parts[3];
|
||||
}
|
||||
@@ -310,18 +327,25 @@ class Event
|
||||
|
||||
$subType ??= false;
|
||||
$subResource ??= false;
|
||||
$subSubType ??= false;
|
||||
$subSubResource ??= false;
|
||||
$attribute ??= false;
|
||||
$action = match (true) {
|
||||
!$hasSubResource && $count > 2 => $parts[2],
|
||||
$hasSubSubResource => $parts[6] ?? false,
|
||||
$hasSubResource && $count > 4 => $parts[4],
|
||||
default => false
|
||||
};
|
||||
|
||||
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'resource' => $resource,
|
||||
'subType' => $subType,
|
||||
'subResource' => $subResource,
|
||||
'subSubType' => $subSubType,
|
||||
'subSubResource' => $subSubResource,
|
||||
'action' => $action,
|
||||
'attribute' => $attribute,
|
||||
];
|
||||
@@ -348,6 +372,8 @@ class Event
|
||||
$resource = $parsed['resource'];
|
||||
$subType = $parsed['subType'];
|
||||
$subResource = $parsed['subResource'];
|
||||
$subSubType = $parsed['subSubType'];
|
||||
$subSubResource = $parsed['subSubResource'];
|
||||
$action = $parsed['action'];
|
||||
$attribute = $parsed['attribute'];
|
||||
|
||||
@@ -359,11 +385,21 @@ class Event
|
||||
throw new InvalidArgumentException("{$subResource} is missing from the params.");
|
||||
}
|
||||
|
||||
if ($subSubResource && !\in_array(\trim($subSubResource, "\[\]"), $paramKeys)) {
|
||||
throw new InvalidArgumentException("{$subSubResource} is missing from the params.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all possible patterns including placeholders.
|
||||
*/
|
||||
if ($action) {
|
||||
if ($subResource) {
|
||||
if ($subSubResource) {
|
||||
if ($attribute) {
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource, $action, $attribute]);
|
||||
}
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource, $action]);
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource]);
|
||||
} elseif ($subResource) {
|
||||
if ($attribute) {
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $action, $attribute]);
|
||||
}
|
||||
@@ -376,6 +412,9 @@ class Event
|
||||
$patterns[] = \implode('.', [$type, $resource, $action, $attribute]);
|
||||
}
|
||||
}
|
||||
if ($subSubResource) {
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource]);
|
||||
}
|
||||
if ($subResource) {
|
||||
$patterns[] = \implode('.', [$type, $resource, $subType, $subResource]);
|
||||
}
|
||||
@@ -395,12 +434,24 @@ class Event
|
||||
$events[] = \str_replace($paramKeys, '*', $eventPattern);
|
||||
foreach ($paramKeys as $key) {
|
||||
foreach ($paramKeys as $current) {
|
||||
if ($current === $key) {
|
||||
continue;
|
||||
if ($subSubResource) {
|
||||
foreach ($paramKeys as $subCurrent) {
|
||||
if ($subCurrent === $current || $subCurrent === $key) {
|
||||
continue;
|
||||
}
|
||||
$filtered1 = \array_filter($paramKeys, fn(string $k) => $k === $subCurrent);
|
||||
$events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered1, '*', $eventPattern));
|
||||
$filtered2 = \array_filter($paramKeys, fn(string $k) => $k === $current);
|
||||
$events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered2, '*', \str_replace($filtered1, '*', $eventPattern)));
|
||||
$events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered2, '*', $eventPattern));
|
||||
}
|
||||
} else {
|
||||
if ($current === $key) {
|
||||
continue;
|
||||
}
|
||||
$filtered = \array_filter($paramKeys, fn(string $k) => $k === $current);
|
||||
$events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered, '*', $eventPattern));
|
||||
}
|
||||
|
||||
$filtered = \array_filter($paramKeys, fn(string $k) => $k === $current);
|
||||
$events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered, '*', $eventPattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,6 +462,9 @@ class Event
|
||||
$events = \array_map(fn (string $event) => \str_replace(['[', ']'], '', $event), $events);
|
||||
$events = \array_unique($events);
|
||||
|
||||
return $events;
|
||||
/**
|
||||
* Force a non-assoc array.
|
||||
*/
|
||||
return \array_values($events);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class Event extends Validator
|
||||
$parts = \explode('.', $value);
|
||||
$count = \count($parts);
|
||||
|
||||
if ($count < 2 || $count > 6) {
|
||||
if ($count < 2 || $count > 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class Event extends Validator
|
||||
$type = $parts[0] ?? false;
|
||||
$resource = $parts[1] ?? false;
|
||||
$hasSubResource = $count > 3 && ($events[$type]['$resource'] ?? false) && ($events[$type][$parts[2]]['$resource'] ?? false);
|
||||
$hasSubSubResource = $count > 5 && $hasSubResource && ($events[$type][$parts[2]][$parts[4]]['$resource'] ?? false);
|
||||
|
||||
if (!$type || !$resource) {
|
||||
return false;
|
||||
@@ -50,21 +51,37 @@ class Event extends Validator
|
||||
if ($hasSubResource) {
|
||||
$subType = $parts[2];
|
||||
$subResource = $parts[3];
|
||||
}
|
||||
|
||||
if ($hasSubSubResource) {
|
||||
$subSubType = $parts[4];
|
||||
$subSubResource = $parts[5];
|
||||
if ($count === 8) {
|
||||
$attribute = $parts[7];
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasSubResource && !$hasSubSubResource) {
|
||||
if ($count === 6) {
|
||||
$attribute = $parts[5];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!$hasSubResource) {
|
||||
if ($count === 4) {
|
||||
$attribute = $parts[3];
|
||||
}
|
||||
}
|
||||
|
||||
$subSubType ??= false;
|
||||
$subSubResource ??= false;
|
||||
$subType ??= false;
|
||||
$subResource ??= false;
|
||||
$attribute ??= false;
|
||||
|
||||
$action = match (true) {
|
||||
!$hasSubResource && $count > 2 => $parts[2],
|
||||
$hasSubSubResource => $parts[6] ?? false,
|
||||
$hasSubResource && $count > 4 => $parts[4],
|
||||
default => false
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ class Exception extends \Exception
|
||||
public const GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope';
|
||||
public const GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded';
|
||||
public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled';
|
||||
public const GENERAL_PHONE_DISABLED = 'general_phone_disabled';
|
||||
public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid';
|
||||
public const GENERAL_QUERY_LIMIT_EXCEEDED = 'general_query_limit_exceeded';
|
||||
public const GENERAL_QUERY_INVALID = 'general_query_invalid';
|
||||
@@ -66,6 +67,8 @@ class Exception extends \Exception
|
||||
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
public const USER_UNAUTHORIZED = 'user_unauthorized';
|
||||
public const USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported';
|
||||
public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
|
||||
public const USER_PHONE_NOT_FOUND = 'user_phone_not_found';
|
||||
|
||||
/** Teams */
|
||||
public const TEAM_NOT_FOUND = 'team_not_found';
|
||||
@@ -112,6 +115,10 @@ class Exception extends \Exception
|
||||
/** Execution */
|
||||
public const EXECUTION_NOT_FOUND = 'execution_not_found';
|
||||
|
||||
/** Databases */
|
||||
public const DATABASE_NOT_FOUND = 'database_not_found';
|
||||
public const DATABASE_ALREADY_EXISTS = 'database_already_exists';
|
||||
|
||||
/** Collections */
|
||||
public const COLLECTION_NOT_FOUND = 'collection_not_found';
|
||||
public const COLLECTION_ALREADY_EXISTS = 'collection_already_exists';
|
||||
|
||||
@@ -242,7 +242,7 @@ class Realtime extends Adapter
|
||||
* @param Document|null $project
|
||||
* @return array
|
||||
*/
|
||||
public static function fromPayload(string $event, Document $payload, Document $project = null, Document $collection = null, Document $bucket = null): array
|
||||
public static function fromPayload(string $event, Document $payload, Document $project = null, Document $database = null, Document $collection = null, Document $bucket = null): array
|
||||
{
|
||||
$channels = [];
|
||||
$roles = [];
|
||||
@@ -271,19 +271,22 @@ class Realtime extends Adapter
|
||||
$roles = ['team:' . $parts[1]];
|
||||
}
|
||||
break;
|
||||
case 'collections':
|
||||
if (in_array($parts[2], ['attributes', 'indexes'])) {
|
||||
case 'databases':
|
||||
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
|
||||
$channels[] = 'console';
|
||||
$projectId = 'console';
|
||||
$roles = ['team:' . $project->getAttribute('teamId')];
|
||||
} elseif ($parts[2] === 'documents') {
|
||||
} elseif (($parts[4] ?? '') === 'documents') {
|
||||
if ($database->isEmpty()) {
|
||||
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
|
||||
}
|
||||
if ($collection->isEmpty()) {
|
||||
throw new \Exception('Collection needs to be passed to Realtime for Document events in the Database.');
|
||||
}
|
||||
|
||||
$channels[] = 'documents';
|
||||
$channels[] = 'collections.' . $payload->getCollection() . '.documents';
|
||||
$channels[] = 'collections.' . $payload->getCollection() . '.documents.' . $payload->getId();
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents';
|
||||
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents.' . $payload->getId();
|
||||
|
||||
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ abstract class Migration
|
||||
'0.14.0' => 'V13',
|
||||
'0.14.1' => 'V13',
|
||||
'0.14.2' => 'V13',
|
||||
'0.15.0' => 'V13'
|
||||
'0.15.0' => 'V14',
|
||||
'0.15.1' => 'V14',
|
||||
'0.15.2' => 'V14'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -104,8 +106,9 @@ abstract class Migration
|
||||
|
||||
foreach ($this->collections as $collection) {
|
||||
if ($collection['$collection'] !== Database::METADATA) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
$sum = 0;
|
||||
$nextDocument = null;
|
||||
$collectionCount = $this->projectDB->count($collection['$id']);
|
||||
@@ -128,7 +131,7 @@ abstract class Migration
|
||||
$old = $document->getArrayCopy();
|
||||
$new = call_user_func($callback, $document);
|
||||
|
||||
if (!self::hasDifference($new->getArrayCopy(), $old)) {
|
||||
if (is_null($new) || !self::hasDifference($new->getArrayCopy(), $old)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,6 +231,87 @@ abstract class Migration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates attribute from collections.php
|
||||
*
|
||||
* @param \Utopia\Database\Database $database
|
||||
* @param string $collectionId
|
||||
* @param string $attributeId
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws \Utopia\Database\Exception\Duplicate
|
||||
* @throws \Utopia\Database\Exception\Limit
|
||||
*/
|
||||
public function createAttributeFromCollection(Database $database, string $collectionId, string $attributeId, string $from = null): void
|
||||
{
|
||||
$from ??= $collectionId;
|
||||
$collection = Config::getParam('collections', [])[$from] ?? null;
|
||||
if (is_null($collection)) {
|
||||
throw new Exception("Collection {$collectionId} not found");
|
||||
}
|
||||
$attributes = $collection['attributes'];
|
||||
|
||||
$attributeKey = array_search($attributeId, array_column($attributes, '$id'));
|
||||
|
||||
if ($attributeKey === false) {
|
||||
throw new Exception("Attribute {$attributeId} not found");
|
||||
}
|
||||
|
||||
$attribute = $attributes[$attributeKey];
|
||||
|
||||
$database->createAttribute(
|
||||
collection: $collectionId,
|
||||
id: $attributeId,
|
||||
type: $attribute['type'],
|
||||
size: $attribute['size'],
|
||||
required: $attribute['required'] ?? false,
|
||||
default: $attribute['default'] ?? null,
|
||||
signed: $attribute['signed'] ?? false,
|
||||
array: $attribute['array'] ?? false,
|
||||
format: $attribute['format'] ?? '',
|
||||
formatOptions: $attribute['formatOptions'] ?? [],
|
||||
filters: $attribute['filters'] ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates index from collections.php
|
||||
*
|
||||
* @param \Utopia\Database\Database $database
|
||||
* @param string $collectionId
|
||||
* @param string $indexId
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws \Utopia\Database\Exception\Duplicate
|
||||
* @throws \Utopia\Database\Exception\Limit
|
||||
*/
|
||||
public function createIndexFromCollection(Database $database, string $collectionId, string $indexId): void
|
||||
{
|
||||
$collection = Config::getParam('collections', [])[$collectionId] ?? null;
|
||||
|
||||
if (is_null($collection)) {
|
||||
throw new Exception("Collection {$collectionId} not found");
|
||||
}
|
||||
$indexes = $collection['indexes'];
|
||||
|
||||
$indexKey = array_search($indexId, array_column($indexes, '$id'));
|
||||
|
||||
if ($indexKey === false) {
|
||||
throw new Exception("Attribute {$indexId} not found");
|
||||
}
|
||||
|
||||
$index = $indexes[$indexKey];
|
||||
|
||||
$database->createIndex(
|
||||
collection: $collectionId,
|
||||
id: $indexId,
|
||||
type: $index['type'],
|
||||
attributes: $index['attributes'],
|
||||
lengths: $index['lengths'] ?? [],
|
||||
orders: $index['orders'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes migration for set project.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,793 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class V14 extends Migration
|
||||
{
|
||||
/**
|
||||
* @var \PDO $pdo
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
public function execute(): void
|
||||
{
|
||||
global $register;
|
||||
$this->pdo = $register->get('db');
|
||||
|
||||
if ($this->project->getId() === 'console' && $this->project->getInternalId() !== 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable SubQueries for Speed.
|
||||
*/
|
||||
foreach (['subQueryAttributes', 'subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships'] 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('Create Default Database Layer');
|
||||
$this->createDatabaseLayer();
|
||||
if ($this->project->getId() !== 'console') {
|
||||
Console::info('Migrating Database Collections');
|
||||
$this->migrateCustomCollections();
|
||||
}
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the default Database for existing Projects.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function createDatabaseLayer(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->projectDB->exists('databases')) {
|
||||
$this->createCollection('databases');
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
if ($this->project->getInternalId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->projectDB->createDocument('databases', new Document([
|
||||
'$id' => 'default',
|
||||
'name' => 'Default',
|
||||
'search' => 'default Default'
|
||||
]));
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates all Files.
|
||||
*
|
||||
* @param \Utopia\Database\Document $bucket
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function migrateBucketFiles(Document $bucket): void
|
||||
{
|
||||
$nextFile = null;
|
||||
do {
|
||||
$documents = $this->projectDB->find("bucket_{$bucket->getInternalId()}", limit: $this->limit, cursor: $nextFile);
|
||||
$count = count($documents);
|
||||
|
||||
foreach ($documents as $document) {
|
||||
go(function (Document $bucket, Document $document) {
|
||||
Console::log("Migrating File {$document->getId()}");
|
||||
try {
|
||||
/**
|
||||
* Migrate $createdAt.
|
||||
*/
|
||||
if (empty($document->getCreatedAt())) {
|
||||
$document->setAttribute('$createdAt', $document->getAttribute('dateCreated'));
|
||||
$this->projectDB->updateDocument("bucket_{$bucket->getInternalId()}", $document->getId(), $document);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
}, $bucket, $document);
|
||||
}
|
||||
|
||||
if ($count !== $this->limit) {
|
||||
$nextFile = null;
|
||||
} else {
|
||||
$nextFile = end($documents);
|
||||
}
|
||||
} while (!is_null($nextFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates all Database Collections.
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function migrateCustomCollections(): void
|
||||
{
|
||||
try {
|
||||
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_collections` RENAME TO `_{$this->project->getInternalId()}_database_1`")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
try {
|
||||
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_collections_perms` RENAME TO `_{$this->project->getInternalId()}_database_1_perms`")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update metadata table.
|
||||
*/
|
||||
try {
|
||||
$this->pdo->prepare("UPDATE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}__metadata`
|
||||
SET
|
||||
_uid = 'database_1',
|
||||
name = 'database_1'
|
||||
WHERE _uid = 'collections';
|
||||
")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Add Database ID for Collections.
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, 'database_1', 'databaseId', 'collections');
|
||||
|
||||
/**
|
||||
* Add Database Internal ID for Collections.
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, 'database_1', 'databaseInternalId', 'collections');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
$nextCollection = null;
|
||||
|
||||
do {
|
||||
$documents = $this->projectDB->find('database_1', limit: $this->limit, cursor: $nextCollection);
|
||||
$count = count($documents);
|
||||
|
||||
\Co\run(function (array $documents) {
|
||||
foreach ($documents as $document) {
|
||||
go(function (Document $collection) {
|
||||
$id = $collection->getId();
|
||||
$internalId = $collection->getInternalId();
|
||||
|
||||
Console::log("- {$id} ({$collection->getAttribute('name')})");
|
||||
|
||||
try {
|
||||
/**
|
||||
* Rename user's colletion table schema
|
||||
*/
|
||||
$this->createNewMetaData("collection_{$internalId}", "database_1_collection_{$internalId}");
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Update metadata table.
|
||||
*/
|
||||
$this->pdo->prepare("UPDATE `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}__metadata`
|
||||
SET
|
||||
_uid = 'database_1_collection_{$internalId}',
|
||||
name = 'database_1_collection_{$internalId}'
|
||||
WHERE _uid = 'collection_{$internalId}';
|
||||
")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Update internal ID's.
|
||||
*/
|
||||
$collection
|
||||
->setAttribute('databaseId', 'default')
|
||||
->setAttribute('databaseInternalId', '1');
|
||||
$this->projectDB->updateDocument('database_1', $collection->getId(), $collection);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
/**
|
||||
* Migrate Attributes
|
||||
*/
|
||||
$this->migrateAttributesAndCollections('attributes', $collection);
|
||||
/**
|
||||
* Migrate Indexes
|
||||
*/
|
||||
$this->migrateAttributesAndCollections('indexes', $collection);
|
||||
}, $document);
|
||||
}
|
||||
}, $documents);
|
||||
|
||||
if ($count !== $this->limit) {
|
||||
$nextCollection = null;
|
||||
} else {
|
||||
$nextCollection = end($documents);
|
||||
}
|
||||
} while (!is_null($nextCollection));
|
||||
}
|
||||
|
||||
protected function migrateAttributesAndCollections(string $type, Document $collection): void
|
||||
{
|
||||
/**
|
||||
* Offset pagination instead of cursor, since documents are re-created!
|
||||
*/
|
||||
$offset = 0;
|
||||
$attributesCount = $this->projectDB->count($type, queries: [new Query('collectionId', Query::TYPE_EQUAL, [$collection->getId()])]);
|
||||
|
||||
do {
|
||||
$documents = $this->projectDB->find($type, limit: $this->limit, offset: $offset, queries: [new Query('collectionId', Query::TYPE_EQUAL, [$collection->getId()])]);
|
||||
$offset += $this->limit;
|
||||
|
||||
foreach ($documents as $document) {
|
||||
go(function (Document $document, string $internalId, string $type) {
|
||||
try {
|
||||
/**
|
||||
* Skip already migrated Documents.
|
||||
*/
|
||||
if (!is_null($document->getAttribute('databaseId'))) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'collectionInternalId' for Subqueries.
|
||||
*/
|
||||
$document->setAttribute('collectionInternalId', $internalId);
|
||||
/**
|
||||
* Add Internal ID 'databaseInternalId' for Subqueries.
|
||||
*/
|
||||
$document->setAttribute('databaseInternalId', '1');
|
||||
/**
|
||||
* Add Internal ID 'databaseId'.
|
||||
*/
|
||||
$document->setAttribute('databaseId', 'default');
|
||||
|
||||
/**
|
||||
* Re-create Attribute.
|
||||
*/
|
||||
$this->projectDB->deleteDocument($document->getCollection(), $document->getId());
|
||||
$this->projectDB->createDocument($document->getCollection(), $document->setAttribute('$id', "1_{$internalId}_{$document->getAttribute('key')}"));
|
||||
} catch (\Throwable $th) {
|
||||
Console::error("Failed to {$type} document: " . $th->getMessage());
|
||||
}
|
||||
}, $document, $collection->getInternalId(), $type);
|
||||
}
|
||||
} while ($offset < $attributesCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Collections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function migrateCollections(): void
|
||||
{
|
||||
foreach ($this->collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("- {$id}");
|
||||
|
||||
$this->createNewMetaData($id);
|
||||
|
||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
switch ($id) {
|
||||
case 'attributes':
|
||||
case 'indexes':
|
||||
try {
|
||||
/**
|
||||
* Create 'databaseInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'databaseId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'databaseInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create 'databaseInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'databaseInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'databaseInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Create 'collectionInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'collectionInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'collectionInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_collection' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_collection');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_db_collection');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_collection' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'projects':
|
||||
try {
|
||||
/**
|
||||
* Create 'teamInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'teamInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'teamInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'platforms':
|
||||
case 'domains':
|
||||
try {
|
||||
/**
|
||||
* Create 'projectInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'projectInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'projectInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_project' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_project');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_project');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_project' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'keys':
|
||||
try {
|
||||
/**
|
||||
* Create 'projectInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'projectInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'projectInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create 'expire' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'expire');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'expire' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_project' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_project');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_project');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_project' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'webhooks':
|
||||
try {
|
||||
/**
|
||||
* Create 'signatureKey' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'signatureKey');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'signatureKey' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create 'projectInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'projectInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'projectInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_project' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_project');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_project');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_project' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'users':
|
||||
try {
|
||||
/**
|
||||
* Create 'phone' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'phone');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'phone' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create 'phoneVerification' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'phoneVerification');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'phoneVerification' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create '_key_phone' index
|
||||
*/
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_phone');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_phone' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'tokens':
|
||||
case 'sessions':
|
||||
try {
|
||||
/**
|
||||
* Create 'userInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'userInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_user' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_user');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_user');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_user' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'memberships':
|
||||
try {
|
||||
/**
|
||||
* Create 'teamInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'teamInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'teamInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create 'userInternalId' attribute
|
||||
*/
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'userInternalId');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_unique' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_unique');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_unique');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_unique' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_team' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_team');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_team');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_team' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Re-Create '_key_user' index
|
||||
*/
|
||||
@$this->projectDB->deleteIndex($id, '_key_user');
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_user');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_user' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
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 Project version number.
|
||||
*/
|
||||
$document->setAttribute('version', '0.15.0');
|
||||
|
||||
if (!empty($document->getAttribute('teamId')) && is_null($document->getAttribute('teamInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('teams', $document->getAttribute('teamId'))->getInternalId();
|
||||
$document->setAttribute('teamInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'keys':
|
||||
/**
|
||||
* Add new 'expire' attribute and default to never (0).
|
||||
*/
|
||||
if (is_null($document->getAttribute('expire'))) {
|
||||
$document->setAttribute('expire', 0);
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'projectId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('projectId')) && is_null($document->getAttribute('projectInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('projects', $document->getAttribute('projectId'))->getInternalId();
|
||||
$document->setAttribute('projectInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'audit':
|
||||
/**
|
||||
* Add Database Layer to collection resource.
|
||||
*/
|
||||
if (str_starts_with($document->getAttribute('resource'), 'collection/')) {
|
||||
$document
|
||||
->setAttribute('resource', "database/default/{$document->getAttribute('resource')}")
|
||||
->setAttribute('event', "databases.default.{$document->getAttribute('event')}");
|
||||
}
|
||||
|
||||
if (str_starts_with($document->getAttribute('resource'), 'document/')) {
|
||||
$collectionId = explode('.', $document->getAttribute('event'))[1];
|
||||
$document
|
||||
->setAttribute('resource', "database/default/collection/{$collectionId}/{$document->getAttribute('resource')}")
|
||||
->setAttribute('event', "databases.default.{$document->getAttribute('event')}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'stats':
|
||||
/**
|
||||
* Add Database Layer to stats metric.
|
||||
*/
|
||||
if (str_starts_with($document->getAttribute('metric'), 'database.')) {
|
||||
$metric = ltrim($document->getAttribute('metric'), 'database.');
|
||||
$document->setAttribute('metric', "databases.default.{$metric}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'webhooks':
|
||||
/**
|
||||
* Add new 'signatureKey' attribute and generate a random value.
|
||||
*/
|
||||
if (empty($document->getAttribute('signatureKey'))) {
|
||||
$document->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'projectId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('projectId')) && is_null($document->getAttribute('projectInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('projects', $document->getAttribute('projectId'))->getInternalId();
|
||||
$document->setAttribute('projectInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'domains':
|
||||
/**
|
||||
* Add Internal ID 'projectId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('projectId')) && is_null($document->getAttribute('projectInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('projects', $document->getAttribute('projectId'))->getInternalId();
|
||||
$document->setAttribute('projectInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'tokens':
|
||||
case 'sessions':
|
||||
/**
|
||||
* Add Internal ID 'userId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('userId')) && is_null($document->getAttribute('userInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('users', $document->getAttribute('userId'))->getInternalId();
|
||||
$document->setAttribute('userInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'memberships':
|
||||
/**
|
||||
* Add Internal ID 'userId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('userId')) && is_null($document->getAttribute('userInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('users', $document->getAttribute('userId'))->getInternalId();
|
||||
$document->setAttribute('userInternalId', $internalId);
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'teamId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('teamId')) && is_null($document->getAttribute('teamInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('teams', $document->getAttribute('teamId'))->getInternalId();
|
||||
$document->setAttribute('teamInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'platforms':
|
||||
/**
|
||||
* Migrate dateCreated to $createdAt.
|
||||
*/
|
||||
if (empty($document->getCreatedAt())) {
|
||||
$document->setAttribute('$createdAt', $document->getAttribute('dateCreated'));
|
||||
}
|
||||
/**
|
||||
* Migrate dateUpdated to $updatedAt.
|
||||
*/
|
||||
if (empty($document->getUpdatedAt())) {
|
||||
$document->setAttribute('$updatedAt', $document->getAttribute('dateUpdated'));
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'projectId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('projectId')) && is_null($document->getAttribute('projectInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('projects', $document->getAttribute('projectId'))->getInternalId();
|
||||
$document->setAttribute('projectInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'buckets':
|
||||
/**
|
||||
* Migrate dateCreated to $createdAt.
|
||||
*/
|
||||
if (empty($document->getCreatedAt())) {
|
||||
$document->setAttribute('$createdAt', $document->getAttribute('dateCreated'));
|
||||
}
|
||||
/**
|
||||
* Migrate dateUpdated to $updatedAt.
|
||||
*/
|
||||
if (empty($document->getUpdatedAt())) {
|
||||
$document->setAttribute('$updatedAt', $document->getAttribute('dateUpdated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Storage Buckets to use Internal ID.
|
||||
*/
|
||||
$internalId = $this->projectDB->getDocument('buckets', $document->getId())->getInternalId();
|
||||
$this->createNewMetaData("bucket_{$internalId}");
|
||||
|
||||
/**
|
||||
* Migrate all Storage Bucket Files.
|
||||
*/
|
||||
$this->migrateBucketFiles($document);
|
||||
|
||||
break;
|
||||
case 'users':
|
||||
/**
|
||||
* Set 'phoneVerification' to false if not set.
|
||||
*/
|
||||
if (is_null($document->getAttribute('phoneVerification'))) {
|
||||
$document->setAttribute('phoneVerification', false);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'functions':
|
||||
/**
|
||||
* Migrate dateCreated to $createdAt.
|
||||
*/
|
||||
if (empty($document->getCreatedAt())) {
|
||||
$document->setAttribute('$createdAt', $document->getAttribute('dateCreated'));
|
||||
}
|
||||
/**
|
||||
* Migrate dateUpdated to $updatedAt.
|
||||
*/
|
||||
if (empty($document->getUpdatedAt())) {
|
||||
$document->setAttribute('$updatedAt', $document->getAttribute('dateUpdated'));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'deployments':
|
||||
case 'executions':
|
||||
case 'teams':
|
||||
/**
|
||||
* Migrate dateCreated to $createdAt.
|
||||
*/
|
||||
if (empty($document->getCreatedAt())) {
|
||||
$document->setAttribute('$createdAt', $document->getAttribute('dateCreated'));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new metadata that was introduced for a collection and enforces the Internal ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @return void
|
||||
*/
|
||||
protected function createNewMetaData(string $id, string $to = null): void
|
||||
{
|
||||
$to ??= $id;
|
||||
/**
|
||||
* Skip files collection.
|
||||
*/
|
||||
if (in_array($id, ['files', 'databases'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Replace project UID with Internal ID.
|
||||
*/
|
||||
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getId()}_{$id}` RENAME TO `_{$this->project->getInternalId()}_{$to}`")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Replace project UID with Internal ID on permissions table.
|
||||
*/
|
||||
$this->pdo->prepare("ALTER TABLE IF EXISTS `{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getId()}_{$id}_perms` RENAME TO `_{$this->project->getInternalId()}_{$to}_perms`")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Add _createdAt attribute.
|
||||
*/
|
||||
$this->pdo->prepare("ALTER TABLE `_{$this->project->getInternalId()}_{$to}` ADD COLUMN IF NOT EXISTS `_createdAt` int unsigned DEFAULT NULL")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Add _updatedAt attribute.
|
||||
*/
|
||||
$this->pdo->prepare("ALTER TABLE `_{$this->project->getInternalId()}_{$to}` ADD COLUMN IF NOT EXISTS `_updatedAt` int unsigned DEFAULT NULL")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create index for _createdAt.
|
||||
*/
|
||||
$this->pdo->prepare("CREATE INDEX IF NOT EXISTS `_created_at` ON `_{$this->project->getInternalId()}_{$to}` (`_createdAt`)")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* Create index for _updatedAt.
|
||||
*/
|
||||
$this->pdo->prepare("CREATE INDEX IF NOT EXISTS `_updated_at` ON `_{$this->project->getInternalId()}_{$to}` (`_updatedAt`)")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Migrating {$id} Collection: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,40 +8,22 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
abstract class Format
|
||||
{
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $services;
|
||||
protected App $app;
|
||||
|
||||
/**
|
||||
* @var Route[]
|
||||
*/
|
||||
protected $routes;
|
||||
protected array $routes;
|
||||
|
||||
/**
|
||||
* @var Model[]
|
||||
*/
|
||||
protected $models;
|
||||
protected array $models;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $keys;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $authCount;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params = [
|
||||
protected array $services;
|
||||
protected array $keys;
|
||||
protected int $authCount;
|
||||
protected array $params = [
|
||||
'name' => '',
|
||||
'description' => '',
|
||||
'endpoint' => 'https://localhost',
|
||||
@@ -56,14 +38,6 @@ abstract class Format
|
||||
'license.url' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param App $app
|
||||
* @param array $services
|
||||
* @param Route[] $routes
|
||||
* @param Model[] $models
|
||||
* @param array $keys
|
||||
* @param int $authCount
|
||||
*/
|
||||
public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount)
|
||||
{
|
||||
$this->app = $app;
|
||||
@@ -121,10 +95,6 @@ abstract class Format
|
||||
*/
|
||||
public function getParam(string $key, string $default = ''): string
|
||||
{
|
||||
if (!isset($this->params[$key])) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->params[$key];
|
||||
return $this->params[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,35 +4,40 @@ namespace Appwrite\Specification\Format;
|
||||
|
||||
use Appwrite\Specification\Format;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Validator;
|
||||
|
||||
class OpenAPI3 extends Format
|
||||
{
|
||||
/**
|
||||
* Get Name.
|
||||
*
|
||||
* Get format name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Open API 3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse
|
||||
*
|
||||
* Parses Appwrite App to given format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getNestedModels(Model $model, array &$usedModels): void
|
||||
{
|
||||
foreach ($model->getRules() as $rule) {
|
||||
if (
|
||||
in_array($model->getType(), $usedModels)
|
||||
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])
|
||||
) {
|
||||
$usedModels[] = $rule['type'];
|
||||
foreach ($this->models as $m) {
|
||||
if ($m->getType() === $rule['type']) {
|
||||
$this->getNestedModels($m, $usedModels);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function parse(): array
|
||||
{
|
||||
/**
|
||||
* Specifications (v3.0.0):
|
||||
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md
|
||||
*/
|
||||
* Specifications (v3.0.0):
|
||||
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md
|
||||
*/
|
||||
$output = [
|
||||
'openapi' => '3.0.0',
|
||||
'info' => [
|
||||
@@ -89,7 +94,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
$usedModels = [];
|
||||
|
||||
foreach ($this->routes as $route) { /** @var \Utopia\Route $route */
|
||||
foreach ($this->routes as $route) {
|
||||
$url = \str_replace('/v1', '', $route->getPath());
|
||||
$scope = $route->getLabel('scope', '');
|
||||
$hide = $route->getLabel('sdk.hide', false);
|
||||
@@ -104,34 +109,32 @@ class OpenAPI3 extends Format
|
||||
$produces = $route->getLabel('sdk.response.type', null);
|
||||
$model = $route->getLabel('sdk.response.model', 'none');
|
||||
$routeSecurity = $route->getLabel('sdk.auth', []);
|
||||
$sdkPlatofrms = [];
|
||||
$sdkPlatforms = [];
|
||||
|
||||
foreach ($routeSecurity as $value) {
|
||||
switch ($value) {
|
||||
case APP_AUTH_TYPE_SESSION:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
|
||||
break;
|
||||
case APP_AUTH_TYPE_KEY:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
|
||||
$sdkPlatforms[] = APP_PLATFORM_SERVER;
|
||||
break;
|
||||
case APP_AUTH_TYPE_JWT:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
|
||||
$sdkPlatforms[] = APP_PLATFORM_SERVER;
|
||||
break;
|
||||
case APP_AUTH_TYPE_ADMIN:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CONSOLE;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CONSOLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($routeSecurity)) {
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
|
||||
}
|
||||
|
||||
$temp = [
|
||||
'summary' => $route->getDesc(),
|
||||
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id),
|
||||
// 'consumes' => [],
|
||||
// 'produces' => [$produces],
|
||||
'tags' => [$route->getLabel('sdk.namespace', 'default')],
|
||||
'description' => ($desc) ? \file_get_contents($desc) : '',
|
||||
'responses' => [],
|
||||
@@ -146,20 +149,14 @@ class OpenAPI3 extends Format
|
||||
'rate-time' => $route->getLabel('abuse-time', 3600),
|
||||
'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'),
|
||||
'scope' => $route->getLabel('scope', ''),
|
||||
'platforms' => $sdkPlatofrms,
|
||||
'platforms' => $sdkPlatforms,
|
||||
'packaging' => $route->getLabel('sdk.packaging', false),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->models as $key => $value) {
|
||||
foreach ($this->models as $value) {
|
||||
if (\is_array($model)) {
|
||||
$model = \array_map(function ($m) use ($value) {
|
||||
if ($m === $value->getType()) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $m;
|
||||
}, $model);
|
||||
$model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model);
|
||||
} else {
|
||||
if ($value->getType() === $model) {
|
||||
$model = $value;
|
||||
@@ -168,9 +165,9 @@ class OpenAPI3 extends Format
|
||||
}
|
||||
}
|
||||
|
||||
if (!(\is_array($model)) && $model->isNone()) {
|
||||
if (!(\is_array($model)) && $model->isNone()) {
|
||||
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
|
||||
'description' => (in_array($produces, [
|
||||
'description' => in_array($produces, [
|
||||
'image/*',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
@@ -179,16 +176,11 @@ class OpenAPI3 extends Format
|
||||
'image/svg-x',
|
||||
'image/x-icon',
|
||||
'image/bmp',
|
||||
])) ? 'Image' : 'File',
|
||||
// 'schema' => [
|
||||
// 'type' => 'file'
|
||||
// ],
|
||||
]) ? 'Image' : 'File',
|
||||
];
|
||||
} else {
|
||||
if (\is_array($model)) {
|
||||
$modelDescription = \join(', or ', \array_map(function ($m) {
|
||||
return $m->getName();
|
||||
}, $model));
|
||||
$modelDescription = \join(', or ', \array_map(fn ($m) => $m->getName(), $model));
|
||||
|
||||
// model has multiple possible responses, we will use oneOf
|
||||
foreach ($model as $m) {
|
||||
@@ -200,9 +192,7 @@ class OpenAPI3 extends Format
|
||||
'content' => [
|
||||
$produces => [
|
||||
'schema' => [
|
||||
'oneOf' => \array_map(function ($m) {
|
||||
return ['$ref' => '#/components/schemas/' . $m->getType()];
|
||||
}, $model)
|
||||
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model)
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -255,7 +245,10 @@ class OpenAPI3 extends Format
|
||||
$bodyRequired = [];
|
||||
|
||||
foreach ($route->getParams() as $name => $param) { // Set params
|
||||
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; /* @var $validator \Utopia\Validator */
|
||||
/**
|
||||
* @var \Utopia\Validator $validator
|
||||
*/
|
||||
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
|
||||
|
||||
$node = [
|
||||
'name' => $name,
|
||||
@@ -263,6 +256,12 @@ class OpenAPI3 extends Format
|
||||
'required' => !$param['optional'],
|
||||
];
|
||||
|
||||
foreach ($this->services as $service) {
|
||||
if ($route->getLabel('sdk.namespace', 'default') === $service['name'] && in_array($name, $service['x-globalAttributes'] ?? [])) {
|
||||
$node['x-global'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Utopia\Validator\Text':
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
@@ -323,7 +322,8 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['format'] = 'password';
|
||||
$node['schema']['x-example'] = 'password';
|
||||
break;
|
||||
case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */
|
||||
case 'Utopia\Validator\Range':
|
||||
/** @var \Utopia\Validator\Range $validator */
|
||||
$node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
|
||||
$node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
|
||||
$node['schema']['x-example'] = $validator->getMin();
|
||||
@@ -345,7 +345,8 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['format'] = 'url';
|
||||
$node['schema']['x-example'] = 'https://example.com';
|
||||
break;
|
||||
case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */
|
||||
case 'Utopia\Validator\WhiteList':
|
||||
/** @var \Utopia\Validator\WhiteList $validator */
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['x-example'] = $validator->getList()[0];
|
||||
|
||||
@@ -390,6 +391,10 @@ class OpenAPI3 extends Format
|
||||
if (\array_key_exists('items', $node['schema'])) {
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['items'] = $node['schema']['items'];
|
||||
}
|
||||
|
||||
if ($node['x-global'] ?? false) {
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
|
||||
@@ -403,20 +408,11 @@ class OpenAPI3 extends Format
|
||||
$temp['requestBody'] = $body;
|
||||
}
|
||||
|
||||
//$temp['consumes'] = $consumes;
|
||||
|
||||
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
|
||||
}
|
||||
|
||||
foreach ($this->models as $model) {
|
||||
foreach ($model->getRules() as $rule) {
|
||||
if (
|
||||
in_array($model->getType(), $usedModels)
|
||||
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])
|
||||
) {
|
||||
$usedModels[] = $rule['type'];
|
||||
}
|
||||
}
|
||||
$this->getNestedModels($model, $usedModels);
|
||||
}
|
||||
|
||||
foreach ($this->models as $model) {
|
||||
|
||||
@@ -4,34 +4,56 @@ namespace Appwrite\Specification\Format;
|
||||
|
||||
use Appwrite\Specification\Format;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Validator;
|
||||
|
||||
class Swagger2 extends Format
|
||||
{
|
||||
/**
|
||||
* Get Name.
|
||||
*
|
||||
* Get format name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Swagger 2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse
|
||||
*
|
||||
* Parses Appwrite App to given format
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getNestedModels(Model $model, array &$usedModels): void
|
||||
{
|
||||
foreach ($model->getRules() as $rule) {
|
||||
if (!in_array($model->getType(), $usedModels)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
foreach ($rule['type'] as $ruleType) {
|
||||
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
|
||||
$usedModels[] = $ruleType;
|
||||
|
||||
foreach ($this->models as $m) {
|
||||
if ($m->getType() === $ruleType) {
|
||||
$this->getNestedModels($m, $usedModels);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
|
||||
$usedModels[] = $rule['type'];
|
||||
|
||||
foreach ($this->models as $m) {
|
||||
if ($m->getType() === $rule['type']) {
|
||||
$this->getNestedModels($m, $usedModels);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function parse(): array
|
||||
{
|
||||
/*
|
||||
* Specifications (v3.0.0):
|
||||
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md
|
||||
* Specifications (v2.0):
|
||||
* https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md
|
||||
*/
|
||||
$output = [
|
||||
'swagger' => '2.0',
|
||||
@@ -87,7 +109,8 @@ class Swagger2 extends Format
|
||||
|
||||
$usedModels = [];
|
||||
|
||||
foreach ($this->routes as $route) { /** @var \Utopia\Route $route */
|
||||
foreach ($this->routes as $route) {
|
||||
/** @var \Utopia\Route $route */
|
||||
$url = \str_replace('/v1', '', $route->getPath());
|
||||
$scope = $route->getLabel('scope', '');
|
||||
$hide = $route->getLabel('sdk.hide', false);
|
||||
@@ -102,27 +125,27 @@ class Swagger2 extends Format
|
||||
$produces = $route->getLabel('sdk.response.type', null);
|
||||
$model = $route->getLabel('sdk.response.model', 'none');
|
||||
$routeSecurity = $route->getLabel('sdk.auth', []);
|
||||
$sdkPlatofrms = [];
|
||||
$sdkPlatforms = [];
|
||||
|
||||
foreach ($routeSecurity as $value) {
|
||||
switch ($value) {
|
||||
case APP_AUTH_TYPE_SESSION:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
|
||||
break;
|
||||
case APP_AUTH_TYPE_KEY:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
|
||||
$sdkPlatforms[] = APP_PLATFORM_SERVER;
|
||||
break;
|
||||
case APP_AUTH_TYPE_JWT:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
|
||||
$sdkPlatforms[] = APP_PLATFORM_SERVER;
|
||||
break;
|
||||
case APP_AUTH_TYPE_ADMIN:
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CONSOLE;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CONSOLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($routeSecurity)) {
|
||||
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
|
||||
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
|
||||
}
|
||||
|
||||
$temp = [
|
||||
@@ -144,7 +167,7 @@ class Swagger2 extends Format
|
||||
'rate-time' => $route->getLabel('abuse-time', 3600),
|
||||
'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'),
|
||||
'scope' => $route->getLabel('scope', ''),
|
||||
'platforms' => $sdkPlatofrms,
|
||||
'platforms' => $sdkPlatforms,
|
||||
'packaging' => $route->getLabel('sdk.packaging', false),
|
||||
],
|
||||
];
|
||||
@@ -153,14 +176,9 @@ class Swagger2 extends Format
|
||||
$temp['produces'][] = $produces;
|
||||
}
|
||||
|
||||
foreach ($this->models as $key => $value) {
|
||||
foreach ($this->models as $value) {
|
||||
if (\is_array($model)) {
|
||||
$model = \array_map(function ($m) use ($value) {
|
||||
if ($m === $value->getType()) {
|
||||
return $value;
|
||||
}
|
||||
return $m;
|
||||
}, $model);
|
||||
$model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model);
|
||||
} else {
|
||||
if ($value->getType() === $model) {
|
||||
$model = $value;
|
||||
@@ -171,7 +189,7 @@ class Swagger2 extends Format
|
||||
|
||||
if (!(\is_array($model)) && $model->isNone()) {
|
||||
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
|
||||
'description' => (in_array($produces, [
|
||||
'description' => in_array($produces, [
|
||||
'image/*',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
@@ -180,16 +198,14 @@ class Swagger2 extends Format
|
||||
'image/svg-x',
|
||||
'image/x-icon',
|
||||
'image/bmp',
|
||||
])) ? 'Image' : 'File',
|
||||
]) ? 'Image' : 'File',
|
||||
'schema' => [
|
||||
'type' => 'file'
|
||||
],
|
||||
];
|
||||
} else {
|
||||
if (\is_array($model)) {
|
||||
$modelDescription = \join(', or ', \array_map(function ($m) {
|
||||
return $m->getName();
|
||||
}, $model));
|
||||
$modelDescription = \join(', or ', \array_map(fn ($m) => $m->getName(), $model));
|
||||
// model has multiple possible responses, we will use oneOf
|
||||
foreach ($model as $m) {
|
||||
$usedModels[] = $m->getType();
|
||||
@@ -244,7 +260,8 @@ class Swagger2 extends Format
|
||||
$bodyRequired = [];
|
||||
|
||||
foreach ($route->getParams() as $name => $param) { // Set params
|
||||
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; /** @var \Utopia\Validator $validator */
|
||||
/** @var \Utopia\Validator $validator */
|
||||
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
|
||||
|
||||
$node = [
|
||||
'name' => $name,
|
||||
@@ -252,6 +269,12 @@ class Swagger2 extends Format
|
||||
'required' => !$param['optional'],
|
||||
];
|
||||
|
||||
foreach ($this->services as $service) {
|
||||
if ($route->getLabel('sdk.namespace', 'default') === $service['name'] && in_array($name, $service['x-globalAttributes'] ?? [])) {
|
||||
$node['x-global'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
switch ((!empty($validator)) ? \get_class($validator) : '') {
|
||||
case 'Utopia\Validator\Text':
|
||||
$node['type'] = $validator->getType();
|
||||
@@ -288,7 +311,6 @@ class Swagger2 extends Format
|
||||
$node['type'] = 'object';
|
||||
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
$node['x-example'] = '{}';
|
||||
//$node['format'] = 'json';
|
||||
break;
|
||||
case 'Utopia\Storage\Validator\File':
|
||||
$consumes = ['multipart/form-data'];
|
||||
@@ -314,7 +336,8 @@ class Swagger2 extends Format
|
||||
$node['format'] = 'password';
|
||||
$node['x-example'] = 'password';
|
||||
break;
|
||||
case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */
|
||||
case 'Utopia\Validator\Range':
|
||||
/** @var \Utopia\Validator\Range $validator */
|
||||
$node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType();
|
||||
$node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float';
|
||||
$node['x-example'] = $validator->getMin();
|
||||
@@ -336,7 +359,8 @@ class Swagger2 extends Format
|
||||
$node['format'] = 'url';
|
||||
$node['x-example'] = 'https://example.com';
|
||||
break;
|
||||
case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */
|
||||
case 'Utopia\Validator\WhiteList':
|
||||
/** @var \Utopia\Validator\WhiteList $validator */
|
||||
$node['type'] = $validator->getType();
|
||||
$node['x-example'] = $validator->getList()[0];
|
||||
|
||||
@@ -378,6 +402,10 @@ class Swagger2 extends Format
|
||||
'x-example' => $node['x-example'] ?? null,
|
||||
];
|
||||
|
||||
if ($node['x-global'] ?? false) {
|
||||
$body['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
|
||||
if (\array_key_exists('items', $node)) {
|
||||
$body['schema']['properties'][$name]['items'] = $node['items'];
|
||||
}
|
||||
@@ -400,23 +428,7 @@ class Swagger2 extends Format
|
||||
}
|
||||
|
||||
foreach ($this->models as $model) {
|
||||
foreach ($model->getRules() as $rule) {
|
||||
if (!in_array($model->getType(), $usedModels)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
foreach ($rule['type'] as $ruleType) {
|
||||
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
|
||||
$usedModels[] = $ruleType;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
|
||||
$usedModels[] = $rule['type'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->getNestedModels($model, $usedModels);
|
||||
}
|
||||
|
||||
foreach ($this->models as $model) {
|
||||
@@ -484,15 +496,11 @@ class Swagger2 extends Format
|
||||
if (\is_array($rule['type'])) {
|
||||
if ($rule['array']) {
|
||||
$items = [
|
||||
'x-anyOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/definitions/' . $type];
|
||||
}, $rule['type'])
|
||||
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
} else {
|
||||
$items = [
|
||||
'x-oneOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/definitions/' . $type];
|
||||
}, $rule['type'])
|
||||
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,14 +4,8 @@ namespace Appwrite\Specification;
|
||||
|
||||
class Specification
|
||||
{
|
||||
/**
|
||||
* @var Format
|
||||
*/
|
||||
protected $format;
|
||||
protected Format $format;
|
||||
|
||||
/**
|
||||
* @param Format $format
|
||||
*/
|
||||
public function __construct(Format $format)
|
||||
{
|
||||
$this->format = $format;
|
||||
|
||||
@@ -113,20 +113,24 @@ class Stats
|
||||
$this->statsd->count('network.all' . $tags, $networkRequestSize + $networkResponseSize);
|
||||
|
||||
$dbMetrics = [
|
||||
'database.collections.create',
|
||||
'database.collections.read',
|
||||
'database.collections.update',
|
||||
'database.collections.delete',
|
||||
'database.documents.create',
|
||||
'database.documents.read',
|
||||
'database.documents.update',
|
||||
'database.documents.delete',
|
||||
'databases.create',
|
||||
'databases.read',
|
||||
'databases.update',
|
||||
'databases.delete',
|
||||
'databases.collections.create',
|
||||
'databases.collections.read',
|
||||
'databases.collections.update',
|
||||
'databases.collections.delete',
|
||||
'databases.documents.create',
|
||||
'databases.documents.read',
|
||||
'databases.documents.update',
|
||||
'databases.documents.delete',
|
||||
];
|
||||
|
||||
foreach ($dbMetrics as $metric) {
|
||||
$value = $this->params[$metric] ?? 0;
|
||||
if ($value >= 1) {
|
||||
$tags = ",projectId={$projectId},collectionId=" . ($this->params['collectionId'] ?? '');
|
||||
$tags = ",projectId={$projectId},collectionId=" . ($this->params['collectionId'] ?? '') . ",databaseId=" . ($this->params['databaseId'] ?? '');
|
||||
$this->statsd->increment($metric . $tags);
|
||||
}
|
||||
}
|
||||
|
||||
+114
-60
@@ -25,45 +25,89 @@ class Usage
|
||||
'executions' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
],
|
||||
'database.collections.create' => [
|
||||
'table' => 'appwrite_usage_database_collections_create',
|
||||
'databases.create' => [
|
||||
'table' => 'appwrite_usage_databases_create',
|
||||
],
|
||||
'database.collections.read' => [
|
||||
'table' => 'appwrite_usage_database_collections_read',
|
||||
'databases.read' => [
|
||||
'table' => 'appwrite_usage_databases_read',
|
||||
],
|
||||
'database.collections.update' => [
|
||||
'table' => 'appwrite_usage_database_collections_update',
|
||||
'databases.update' => [
|
||||
'table' => 'appwrite_usage_databases_update',
|
||||
],
|
||||
'database.collections.delete' => [
|
||||
'table' => 'appwrite_usage_database_collections_delete',
|
||||
'databases.delete' => [
|
||||
'table' => 'appwrite_usage_databases_delete',
|
||||
],
|
||||
'database.documents.create' => [
|
||||
'table' => 'appwrite_usage_database_documents_create',
|
||||
'databases.collections.create' => [
|
||||
'table' => 'appwrite_usage_databases_collections_create',
|
||||
],
|
||||
'database.documents.read' => [
|
||||
'table' => 'appwrite_usage_database_documents_read',
|
||||
'databases.collections.read' => [
|
||||
'table' => 'appwrite_usage_databases_collections_read',
|
||||
],
|
||||
'database.documents.update' => [
|
||||
'table' => 'appwrite_usage_database_documents_update',
|
||||
'databases.collections.update' => [
|
||||
'table' => 'appwrite_usage_databases_collections_update',
|
||||
],
|
||||
'database.documents.delete' => [
|
||||
'table' => 'appwrite_usage_database_documents_delete',
|
||||
'databases.collections.delete' => [
|
||||
'table' => 'appwrite_usage_databases_collections_delete',
|
||||
],
|
||||
'database.collections.collectionId.documents.create' => [
|
||||
'table' => 'appwrite_usage_database_documents_create',
|
||||
'groupBy' => 'collectionId',
|
||||
'databases.documents.create' => [
|
||||
'table' => 'appwrite_usage_databases_documents_create',
|
||||
],
|
||||
'database.collections.collectionId.documents.read' => [
|
||||
'table' => 'appwrite_usage_database_documents_read',
|
||||
'groupBy' => 'collectionId',
|
||||
'databases.documents.read' => [
|
||||
'table' => 'appwrite_usage_databases_documents_read',
|
||||
],
|
||||
'database.collections.collectionId.documents.update' => [
|
||||
'table' => 'appwrite_usage_database_documents_update',
|
||||
'groupBy' => 'collectionId',
|
||||
'databases.documents.update' => [
|
||||
'table' => 'appwrite_usage_databases_documents_update',
|
||||
],
|
||||
'database.collections.collectionId.documents.delete' => [
|
||||
'table' => 'appwrite_usage_database_documents_delete',
|
||||
'groupBy' => 'collectionId',
|
||||
'databases.documents.delete' => [
|
||||
'table' => 'appwrite_usage_databases_documents_delete',
|
||||
],
|
||||
'databases.databaseId.collections.create' => [
|
||||
'table' => 'appwrite_usage_databases_collections_create',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.collections.read' => [
|
||||
'table' => 'appwrite_usage_databases_collections_read',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.collections.update' => [
|
||||
'table' => 'appwrite_usage_databases_collections_update',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.collections.delete' => [
|
||||
'table' => 'appwrite_usage_databases_collections_delete',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.documents.create' => [
|
||||
'table' => 'appwrite_usage_databases_documents_create',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.documents.read' => [
|
||||
'table' => 'appwrite_usage_databases_documents_read',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'database.databaseId.documents.update' => [
|
||||
'table' => 'appwrite_usage_databases_documents_update',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.documents.delete' => [
|
||||
'table' => 'appwrite_usage_databases_documents_delete',
|
||||
'groupBy' => ['databaseId'],
|
||||
],
|
||||
'databases.databaseId.collections.collectionId.documents.create' => [
|
||||
'table' => 'appwrite_usage_databases_documents_create',
|
||||
'groupBy' => ['databaseId', 'collectionId'],
|
||||
],
|
||||
'databases.databaseId.collections.collectionId.documents.read' => [
|
||||
'table' => 'appwrite_usage_databases_documents_read',
|
||||
'groupBy' => ['databaseId', 'collectionId'],
|
||||
],
|
||||
'databases.databaseId.collections.collectionId.documents.update' => [
|
||||
'table' => 'appwrite_usage_databases_documents_update',
|
||||
'groupBy' => ['databaseId', 'collectionId'],
|
||||
],
|
||||
'databases.databaseId.collections.collectionId.documents.delete' => [
|
||||
'table' => 'appwrite_usage_databases_documents_delete',
|
||||
'groupBy' => ['databaseId', 'collectionId'],
|
||||
],
|
||||
'storage.buckets.create' => [
|
||||
'table' => 'appwrite_usage_storage_buckets_create',
|
||||
@@ -91,19 +135,19 @@ class Usage
|
||||
],
|
||||
'storage.buckets.bucketId.files.create' => [
|
||||
'table' => 'appwrite_usage_storage_files_create',
|
||||
'groupBy' => 'bucketId',
|
||||
'groupBy' => ['bucketId'],
|
||||
],
|
||||
'storage.buckets.bucketId.files.read' => [
|
||||
'table' => 'appwrite_usage_storage_files_read',
|
||||
'groupBy' => 'bucketId',
|
||||
'groupBy' => ['bucketId'],
|
||||
],
|
||||
'storage.buckets.bucketId.files.update' => [
|
||||
'table' => 'appwrite_usage_storage_files_update',
|
||||
'groupBy' => 'bucketId',
|
||||
'groupBy' => ['bucketId'],
|
||||
],
|
||||
'storage.buckets.bucketId.files.delete' => [
|
||||
'table' => 'appwrite_usage_storage_files_delete',
|
||||
'groupBy' => 'bucketId',
|
||||
'groupBy' => ['bucketId'],
|
||||
],
|
||||
'users.create' => [
|
||||
'table' => 'appwrite_usage_users_create',
|
||||
@@ -122,22 +166,22 @@ class Usage
|
||||
],
|
||||
'users.sessions.provider.create' => [
|
||||
'table' => 'appwrite_usage_users_sessions_create',
|
||||
'groupBy' => 'provider',
|
||||
'groupBy' => ['provider'],
|
||||
],
|
||||
'users.sessions.delete' => [
|
||||
'table' => 'appwrite_usage_users_sessions_delete',
|
||||
],
|
||||
'functions.functionId.executions' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
'groupBy' => 'functionId',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.compute' => [
|
||||
'table' => 'appwrite_usage_executions_time',
|
||||
'groupBy' => 'functionId',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.failures' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
'groupBy' => 'functionId',
|
||||
'groupBy' => ['functionId'],
|
||||
'filters' => [
|
||||
'functionStatus' => 'failed',
|
||||
],
|
||||
@@ -231,7 +275,7 @@ class Usage
|
||||
$end = DateTime::createFromFormat('U', \strtotime('now'))->format(DateTime::RFC3339);
|
||||
|
||||
$table = $options['table']; //Which influxdb table to query for this metric
|
||||
$groupBy = empty($options['groupBy']) ? '' : ', "' . $options['groupBy'] . '"'; //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
|
||||
$groupBy = empty($options['groupBy']) ? '' : ', ' . implode(', ', array_map(fn($groupBy) => '"' . $groupBy . '" ', $options['groupBy'])); //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
|
||||
|
||||
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
|
||||
if (!empty($filters)) {
|
||||
@@ -247,34 +291,44 @@ class Usage
|
||||
$query .= "AND \"metric_type\"='counter' {$filters} ";
|
||||
$query .= "GROUP BY time({$period['key']}), \"projectId\" {$groupBy} ";
|
||||
$query .= "FILL(null)";
|
||||
$result = $this->influxDB->query($query);
|
||||
|
||||
$points = $result->getPoints();
|
||||
foreach ($points as $point) {
|
||||
$projectId = $point['projectId'];
|
||||
try {
|
||||
$result = $this->influxDB->query($query);
|
||||
$points = $result->getPoints();
|
||||
foreach ($points as $point) {
|
||||
$projectId = $point['projectId'];
|
||||
|
||||
if (!empty($projectId) && $projectId !== 'console') {
|
||||
$metricUpdated = $metric;
|
||||
if (!empty($projectId) && $projectId !== 'console') {
|
||||
$metricUpdated = $metric;
|
||||
|
||||
if (!empty($groupBy)) {
|
||||
$groupedBy = $point[$options['groupBy']] ?? '';
|
||||
if (empty($groupedBy)) {
|
||||
continue;
|
||||
if (!empty($groupBy)) {
|
||||
foreach ($options['groupBy'] as $groupBy) {
|
||||
$groupedBy = $point[$groupBy] ?? '';
|
||||
if (empty($groupedBy)) {
|
||||
continue;
|
||||
}
|
||||
$metricUpdated = str_replace($groupBy, $groupedBy, $metricUpdated);
|
||||
}
|
||||
}
|
||||
$metricUpdated = str_replace($options['groupBy'], $groupedBy, $metric);
|
||||
|
||||
$time = \strtotime($point['time']);
|
||||
$value = (!empty($point['value'])) ? $point['value'] : 0;
|
||||
|
||||
$this->createOrUpdateMetric(
|
||||
$projectId,
|
||||
$time,
|
||||
$period['key'],
|
||||
$metricUpdated,
|
||||
$value,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
$time = \strtotime($point['time']);
|
||||
$value = (!empty($point['value'])) ? $point['value'] : 0;
|
||||
|
||||
$this->createOrUpdateMetric(
|
||||
$projectId,
|
||||
$time,
|
||||
$period['key'],
|
||||
$metricUpdated,
|
||||
$value,
|
||||
0
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) { // if projects are deleted this might fail
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "sync_metric_{$metric}_influxdb");
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,7 @@ class UsageDB extends Usage
|
||||
$period = $options['key'];
|
||||
$time = (int) (floor(time() / $options['multiplier']) * $options['multiplier']);
|
||||
$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);
|
||||
@@ -73,21 +71,15 @@ class UsageDB extends Usage
|
||||
*/
|
||||
private function foreachDocument(string $projectId, string $collection, array $queries, callable $callback): void
|
||||
{
|
||||
if ($projectId === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = 50;
|
||||
$results = [];
|
||||
$sum = $limit;
|
||||
$latestDocument = null;
|
||||
$this->database->setNamespace('_console');
|
||||
$project = $this->database->getDocument('projects', $projectId);
|
||||
$this->database->setNamespace('_' . $project->getInternalId());
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
while ($sum === $limit) {
|
||||
try {
|
||||
$results = $this->database->find($collection, $queries, $limit, cursor: $latestDocument);
|
||||
$results = $this->database->find($collection, $queries, $limit, cursor:$latestDocument);
|
||||
} catch (\Exception $e) {
|
||||
if (is_callable($this->errorHandler)) {
|
||||
call_user_func($this->errorHandler, $e, "fetch_documents_project_{$projectId}_collection_{$collection}");
|
||||
@@ -124,9 +116,7 @@ class UsageDB extends Usage
|
||||
*/
|
||||
private function sum(string $projectId, string $collection, string $attribute, string $metric): int
|
||||
{
|
||||
$this->database->setNamespace('_console');
|
||||
$project = $this->database->getDocument('projects', $projectId);
|
||||
$this->database->setNamespace('_' . $project->getInternalId());
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$sum = (int) $this->database->sum($collection, $attribute);
|
||||
@@ -153,9 +143,7 @@ class UsageDB extends Usage
|
||||
*/
|
||||
private function count(string $projectId, string $collection, string $metric): int
|
||||
{
|
||||
$this->database->setNamespace('_console');
|
||||
$project = $this->database->getDocument('projects', $projectId);
|
||||
$this->database->setNamespace('_' . $project->getInternalId());
|
||||
$this->database->setNamespace('_' . $projectId);
|
||||
|
||||
try {
|
||||
$count = $this->database->count($collection);
|
||||
@@ -245,18 +233,29 @@ class UsageDB extends Usage
|
||||
private function databaseStats(string $projectId): void
|
||||
{
|
||||
$projectDocumentsCount = 0;
|
||||
$projectCollectionsCount = 0;
|
||||
|
||||
$metric = 'database.collections.count';
|
||||
$this->count($projectId, 'collections', $metric);
|
||||
$this->count($projectId, 'databases', 'databases.count');
|
||||
|
||||
$this->foreachDocument($projectId, 'collections', [], function ($collection) use (&$projectDocumentsCount, $projectId,) {
|
||||
$metric = "database.collections.{$collection->getId()}.documents.count";
|
||||
$this->foreachDocument($projectId, 'databases', [], function ($database) use (&$projectDocumentsCount, &$projectCollectionsCount, $projectId) {
|
||||
$metric = "databases.{$database->getId()}.collections.count";
|
||||
$count = $this->count($projectId, 'database_' . $database->getInternalId(), $metric);
|
||||
$projectCollectionsCount += $count;
|
||||
$databaseDocumentsCount = 0;
|
||||
|
||||
$count = $this->count($projectId, 'collection_' . $collection->getInternalId(), $metric);
|
||||
$projectDocumentsCount += $count;
|
||||
$this->foreachDocument($projectId, 'database_' . $database->getInternalId(), [], function ($collection) use (&$projectDocumentsCount, &$databaseDocumentsCount, $projectId, $database) {
|
||||
$metric = "databases.{$database->getId()}.collections.{$collection->getId()}.documents.count";
|
||||
|
||||
$count = $this->count($projectId, 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $metric);
|
||||
$projectDocumentsCount += $count;
|
||||
$databaseDocumentsCount += $count;
|
||||
});
|
||||
|
||||
$this->createOrUpdateMetric($projectId, "databases.{$database->getId()}.documents.count", $databaseDocumentsCount);
|
||||
});
|
||||
|
||||
$this->createOrUpdateMetric($projectId, 'database.documents.count', $projectDocumentsCount);
|
||||
$this->createOrUpdateMetric($projectId, 'databases.collections.count', $projectCollectionsCount);
|
||||
$this->createOrUpdateMetric($projectId, 'databases.documents.count', $projectDocumentsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +267,7 @@ class UsageDB extends Usage
|
||||
public function collect(): void
|
||||
{
|
||||
$this->foreachDocument('console', 'projects', [], function (Document $project) {
|
||||
$projectId = $project->getId();
|
||||
$projectId = $project->getInternalId();
|
||||
|
||||
$this->usersStats($projectId);
|
||||
$this->databaseStats($projectId);
|
||||
|
||||
@@ -30,11 +30,11 @@ use Appwrite\Utopia\Response\Model\AttributeIP;
|
||||
use Appwrite\Utopia\Response\Model\AttributeURL;
|
||||
use Appwrite\Utopia\Response\Model\BaseList;
|
||||
use Appwrite\Utopia\Response\Model\Collection;
|
||||
use Appwrite\Utopia\Response\Model\Database;
|
||||
use Appwrite\Utopia\Response\Model\Continent;
|
||||
use Appwrite\Utopia\Response\Model\Country;
|
||||
use Appwrite\Utopia\Response\Model\Currency;
|
||||
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
|
||||
use Appwrite\Utopia\Response\Model\DocumentList;
|
||||
use Appwrite\Utopia\Response\Model\Domain;
|
||||
use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
@@ -73,6 +73,7 @@ use Appwrite\Utopia\Response\Model\Runtime;
|
||||
use Appwrite\Utopia\Response\Model\UsageBuckets;
|
||||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabase;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
@@ -93,6 +94,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_METRIC_LIST = 'metricList';
|
||||
public const MODEL_ERROR_DEV = 'errorDev';
|
||||
public const MODEL_BASE_LIST = 'baseList';
|
||||
public const MODEL_USAGE_DATABASES = 'usageDatabases';
|
||||
public const MODEL_USAGE_DATABASE = 'usageDatabase';
|
||||
public const MODEL_USAGE_COLLECTION = 'usageCollection';
|
||||
public const MODEL_USAGE_USERS = 'usageUsers';
|
||||
@@ -102,6 +104,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_USAGE_PROJECT = 'usageProject';
|
||||
|
||||
// Database
|
||||
public const MODEL_DATABASE = 'database';
|
||||
public const MODEL_DATABASE_LIST = 'databaseList';
|
||||
public const MODEL_COLLECTION = 'collection';
|
||||
public const MODEL_COLLECTION_LIST = 'collectionList';
|
||||
public const MODEL_INDEX = 'index';
|
||||
@@ -231,6 +235,7 @@ class Response extends SwooleResponse
|
||||
// Lists
|
||||
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
|
||||
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
|
||||
->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE))
|
||||
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
|
||||
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
|
||||
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
|
||||
@@ -256,6 +261,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
|
||||
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
|
||||
// Entities
|
||||
->setModel(new Database())
|
||||
->setModel(new Collection())
|
||||
->setModel(new Attribute())
|
||||
->setModel(new AttributeList())
|
||||
@@ -309,6 +315,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new HealthTime())
|
||||
->setModel(new HealthVersion())
|
||||
->setModel(new Metric())
|
||||
->setModel(new UsageDatabases())
|
||||
->setModel(new UsageDatabase())
|
||||
->setModel(new UsageCollection())
|
||||
->setModel(new UsageUsers())
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
|
||||
class V14 extends Filter
|
||||
{
|
||||
// Convert 0.15 Data format to 0.14 format
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
$parsedResponse = $content;
|
||||
|
||||
switch ($model) {
|
||||
case Response::MODEL_SESSION:
|
||||
case Response::MODEL_TOKEN:
|
||||
$parsedResponse = $this->parseRemoveAttributes($content, ['$createdAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_SESSION_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'domains', ['$createdAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_DOCUMENT:
|
||||
case Response::MODEL_DOMAIN:
|
||||
case Response::MODEL_FUNCTION:
|
||||
case Response::MODEL_TEAM:
|
||||
case Response::MODEL_MEMBERSHIP:
|
||||
case Response::MODEL_PLATFORM:
|
||||
case Response::MODEL_PROJECT:
|
||||
case Response::MODEL_USER:
|
||||
case Response::MODEL_WEBHOOK:
|
||||
$parsedResponse = $this->parseRemoveAttributes($content, ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_DOCUMENT_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'documents', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_DOMAIN_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'domains', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_FUNCTION_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'functions', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_TEAM_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'teams', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_MEMBERSHIP_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'memberships', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_PLATFORM_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'platforms', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_PROJECT_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'projects', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_USER_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'users', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_WEBHOOK_LIST:
|
||||
$parsedResponse = $this->parseRemoveAttributesList($content, 'webhooks', ['$createdAt', '$updatedAt']);
|
||||
|
||||
break;
|
||||
case Response::MODEL_TEAM:
|
||||
case Response::MODEL_EXECUTION:
|
||||
case Response::MODEL_FILE:
|
||||
$parsedResponse = $this->parseCreatedAt($content);
|
||||
break;
|
||||
|
||||
case Response::MODEL_TEAM_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtList($content, 'teams');
|
||||
break;
|
||||
|
||||
case Response::MODEL_EXECUTION_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtList($content, 'executions');
|
||||
break;
|
||||
|
||||
case Response::MODEL_FILE_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtList($content, 'files');
|
||||
break;
|
||||
|
||||
case Response::MODEL_FUNCTION:
|
||||
case Response::MODEL_DEPLOYMENT:
|
||||
case Response::MODEL_BUCKET:
|
||||
$parsedResponse = $this->parseCreatedAtAndUpdatedAt($content);
|
||||
break;
|
||||
|
||||
case Response::MODEL_FUNCTION_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtAndUpdatedAtList($content, 'functions');
|
||||
break;
|
||||
|
||||
case Response::MODEL_DEPLOYMENT_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtAndUpdatedAtList($content, 'deployments');
|
||||
break;
|
||||
|
||||
case Response::MODEL_BUCKET_LIST:
|
||||
$parsedResponse = $this->parseCreatedAtAndUpdatedAtList($content, 'buckets');
|
||||
break;
|
||||
}
|
||||
|
||||
return $parsedResponse;
|
||||
}
|
||||
|
||||
protected function parseRemoveAttributes(array $content, array $attributes)
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
unset($content[$attribute]);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseRemoveAttributesList(array $content, string $property, array $attributes)
|
||||
{
|
||||
$documents = $content[$property];
|
||||
$parsedResponse = [];
|
||||
foreach ($documents as $document) {
|
||||
$parsedResponse[] = $this->parseRemoveAttributes($document, $attributes);
|
||||
}
|
||||
$content[$property] = $parsedResponse;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseCreatedAt(array $content)
|
||||
{
|
||||
$content['dateCreated'] = $content['$createdAt'];
|
||||
unset($content['$createdAt']);
|
||||
unset($content['$updatedAt']);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseCreatedAtList(array $content, string $property)
|
||||
{
|
||||
$documents = $content[$property];
|
||||
$parsedResponse = [];
|
||||
foreach ($documents as $document) {
|
||||
$parsedResponse[] = $this->parseCreatedAt($document);
|
||||
}
|
||||
$content[$property] = $parsedResponse;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseCreatedAtAndUpdatedAt(array $content)
|
||||
{
|
||||
$content['dateCreated'] = $content['$createdAt'];
|
||||
$content['dateUpdated'] = $content['$updatedAt'];
|
||||
unset($content['$createdAt']);
|
||||
unset($content['$updatedAt']);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseCreatedAtAndUpdatedAtList(array $content, string $property)
|
||||
{
|
||||
$documents = $content[$property];
|
||||
$parsedResponse = [];
|
||||
foreach ($documents as $document) {
|
||||
$parsedResponse[] = $this->parseCreatedAtAndUpdatedAt($document);
|
||||
}
|
||||
$content[$property] = $parsedResponse;
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ class Collection extends Model
|
||||
'example' => 'user:608f9da25e7e1',
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databaseId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Database ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Collection name.',
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Database extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Database ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Database name.',
|
||||
'default' => '',
|
||||
'example' => 'My Database',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Collection creation date in Unix timestamp.',
|
||||
'default' => 0,
|
||||
'example' => 1592981250,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Collection update date in Unix timestamp.',
|
||||
'default' => 0,
|
||||
'example' => 1592981250,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Database';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_DATABASE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class UsageDatabases extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('range', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The time range of the usage stats.',
|
||||
'default' => '',
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('databasesCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for total number of documents.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCount', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for total number of collections.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('databasesDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for total number of collections.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('documentsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for documents deleted.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsCreate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for collections created.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsRead', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for collections read.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsUpdate', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for collections updated.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('collectionsDelete', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for collections delete.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'UsageDatabases';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_USAGE_DATABASES;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user