Merge branch 'master' into feat-password-hash-algos

This commit is contained in:
Matej Bačo
2022-07-25 12:37:07 +00:00
908 changed files with 12166 additions and 5800 deletions
+178
View File
@@ -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;
}
}
+35 -6
View File
@@ -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;
}
}
+46
View File
@@ -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}",
]
);
}
}
+1 -1
View File
@@ -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
{
+41
View File
@@ -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
])
);
}
}
+14
View File
@@ -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())
]);
}
+67 -13
View File
@@ -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);
}
}
+19 -2
View File
@@ -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
};
+7
View File
@@ -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';
+9 -6
View File
@@ -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();
}
+87 -3
View File
@@ -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.
*/
+793
View File
@@ -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 -38
View File
@@ -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;
}
}
+56 -60
View File
@@ -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) {
+71 -63
View File
@@ -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 {
+1 -7
View File
@@ -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;
+13 -9
View File
@@ -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
View File
@@ -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;
}
}
}
+24 -25
View File
@@ -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);
+8 -1
View File
@@ -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;
}
}