Merge branch '1.5.x' of https://github.com/appwrite/appwrite into feat-rc-sdks

This commit is contained in:
Torsten Dittmann
2024-02-17 13:25:56 +00:00
31 changed files with 1420 additions and 298 deletions
+4 -1
View File
@@ -192,7 +192,6 @@ class Exception extends \Exception
/** Projects */
public const PROJECT_NOT_FOUND = 'project_not_found';
public const PROJECT_UNKNOWN = 'project_unknown';
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
@@ -252,6 +251,7 @@ class Exception extends \Exception
public const PROVIDER_NOT_FOUND = 'provider_not_found';
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials';
/** Topic */
@@ -274,6 +274,9 @@ class Exception extends \Exception
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
/** Targets */
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
/** Schedules */
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
+1 -1
View File
@@ -78,7 +78,7 @@ abstract class Migration
'1.4.11' => 'V19',
'1.4.12' => 'V19',
'1.4.13' => 'V19',
'1.4.14' => 'V20'
'1.5.0' => 'V20',
];
/**
+289 -66
View File
@@ -2,16 +2,19 @@
namespace Appwrite\Migration\Version;
use Appwrite\Auth\Auth;
use Appwrite\Migration\Migration;
use Exception;
use PDOException;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
class V20 extends Migration
@@ -21,18 +24,14 @@ class V20 extends Migration
*/
public function execute(): void
{
if ($this->project->getInternalId() == 'console') {
return;
}
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables'] as $name) {
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
fn() => null,
fn() => []
);
}
@@ -45,19 +44,239 @@ class V20 extends Migration
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
Console::info('Migrating Functions');
$this->migrateFunctions();
Console::info('Migrating Databases');
$this->migrateDatabases();
Console::info('Migrating Collections');
$this->migrateCollections();
Console::info('Migrating Buckets');
$this->migrateBuckets();
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
}
/**
* Migrate Collections.
*
* @return void
* @throws Exception|Throwable
*/
private function migrateCollections(): void
{
$internalProjectId = $this->project->getInternalId();
$collectionType = match ($internalProjectId) {
'console' => 'console',
default => 'projects',
};
// Support database array type migration (user collections)
foreach (
$this->documentsIterator('attributes', [
Query::equal('array', [true]),
]) as $attribute
) {
$foundIndex = false;
foreach (
$this->documentsIterator('indexes', [
Query::equal('databaseInternalId', [$attribute['databaseInternalId']]),
Query::equal('collectionInternalId', [$attribute['collectionInternalId']]),
]) as $index
) {
if (in_array($attribute['key'], $index['attributes'])) {
$this->projectDB->deleteIndex($index['collectionId'], $index['$id']);
$foundIndex = true;
}
}
if ($foundIndex === true) {
$this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']);
}
}
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
// Support database array type migration
$foundIndex = false;
foreach ($collection['attributes'] ?? [] as $attribute) {
if ($attribute['array'] === true) {
foreach ($collection['indexes'] ?? [] as $index) {
if (in_array($attribute['$id'], $index['attributes'])) {
$this->projectDB->deleteIndex($id, $index['$id']);
$foundIndex = true;
}
}
if ($foundIndex === true) {
$this->projectDB->updateAttribute($id, $attribute['$id'], $attribute['type']);
}
}
}
switch ($id) {
case '_metadata':
$this->createCollection('providers');
$this->createCollection('messages');
$this->createCollection('topics');
$this->createCollection('subscribers');
$this->createCollection('targets');
$this->createCollection('challenges');
break;
case 'cache':
// Create resourceType attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceType');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'resourceType' from {$id}: {$th->getMessage()}");
}
// Create mimeType attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mimeType');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
}
break;
case 'stats':
try {
/**
* Delete 'type' attribute
*/
$this->projectDB->deleteAttribute($id, 'type');
/**
* Alter `signed` internal type on `value` attr
*/
$this->projectDB->updateAttribute(collection: $id, id: 'value', signed: true);
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
// update stats index
$index = '_key_metric_period_time';
try {
$this->projectDB->deleteIndex($id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
try {
$this->createIndexFromCollection($this->projectDB, $id, $index);
} catch (\Throwable $th) {
Console::warning("'$index' from {$id}: {$th->getMessage()}");
}
break;
case 'sessions':
// Create expire attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'expire');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'expire' from {$id}: {$th->getMessage()}");
}
// Create factors attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'factors');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'factors' from {$id}: {$th->getMessage()}");
}
break;
case 'users':
// Create targets attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'targets');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'targets' from {$id}: {$th->getMessage()}");
}
// Create mfa attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'mfa');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'mfa' from {$id}: {$th->getMessage()}");
}
// Create totp attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'totp');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'totp' from {$id}: {$th->getMessage()}");
}
// Create totpVerification attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'totpVerification');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'totpVerification' from {$id}: {$th->getMessage()}");
}
// Create totpSecret attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'totpSecret');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'totpSecret' from {$id}: {$th->getMessage()}");
}
// Create totpBackup attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'totpBackup');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'totpBackup' from {$id}: {$th->getMessage()}");
}
break;
case 'projects':
// Rename providers authProviders to oAuthProviders
try {
$this->projectDB->renameAttribute($id, 'authProviders', 'oAuthProviders');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'oAuthProviders' from {$id}: {$th->getMessage()}");
}
break;
case 'webhooks':
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
$this->createAttributeFromCollection($this->projectDB, $id, 'logs');
$this->createAttributeFromCollection($this->projectDB, $id, 'attempts');
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'webhooks' from {$id}: {$th->getMessage()}");
}
break;
default:
break;
}
usleep(50000);
}
}
/**
* @return void
* @throws Authorization
@@ -89,7 +308,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
$sessionsDeleted = $query['value'] ?? 0;
$sessionsDeleted = $query['value'] ?? 0;
$value = $sessionsCreated - $sessionsDeleted;
$this->createInfMetric('sessions', $value);
}
@@ -115,8 +334,8 @@ class V20 extends Migration
'$id' => $id,
'metric' => $metric,
'period' => 'inf',
'value' => $value,
'time' => null,
'value' => $value,
'time' => null,
'region' => 'default',
]));
} catch (Duplicate $th) {
@@ -132,6 +351,7 @@ class V20 extends Migration
*/
protected function migrateUsageMetrics(string $from, string $to): void
{
/**
* inf metric
*/
@@ -159,7 +379,7 @@ class V20 extends Migration
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument !== null) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$stats = $this->projectDB->find('stats', \array_merge($paginationQueries, [
Query::equal('metric', [$from]),
@@ -182,11 +402,12 @@ class V20 extends Migration
Console::warning("Error while updating metric {$from} " . $th->getMessage());
}
}
/**
* Migrate functions.
*
* @return void
* @throws \Exception
* @throws Exception
*/
private function migrateFunctions(): void
{
@@ -215,7 +436,7 @@ class V20 extends Migration
* Migrate Databases.
*
* @return void
* @throws \Exception
* @throws Exception
*/
private function migrateDatabases(): void
{
@@ -241,7 +462,7 @@ class V20 extends Migration
Console::log("Migrating Collections of {$collectionTable} {$collection->getId()} ({$collection->getAttribute('name')})");
// Collection level
$collectionId = $collection->getId() ;
$collectionId = $collection->getId();
$collectionInternalId = $collection->getInternalId();
$this->migrateUsageMetrics("documents.$databaseId/$collectionId.count.total", "$databaseInternalId.$collectionInternalId.documents");
@@ -250,53 +471,10 @@ class V20 extends Migration
}
/**
* Migrate Collections.
* Migrating Buckets.
*
* @return void
* @throws \Exception
*/
private function migrateCollections(): void
{
$internalProjectId = $this->project->getInternalId();
$collectionType = match ($internalProjectId) {
'console' => 'console',
default => 'projects',
};
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
switch ($id) {
case 'stats':
try {
/**
* Delete 'type' attribute
*/
$this->projectDB->deleteAttribute($id, 'type');
/**
* Alter `signed` internal type on `value` attr
*/
$this->projectDB->updateAttribute($id, 'value', null, null, null, null, true);
$this->projectDB->deleteCachedCollection($id);
} catch (Throwable $th) {
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
break;
}
}
}
/**
* Migrating all Bucket tables.
*
* @return void
* @throws \Exception
* @throws Exception
* @throws PDOException
*/
protected function migrateBuckets(): void
@@ -305,7 +483,6 @@ class V20 extends Migration
$this->migrateUsageMetrics('buckets.$all.count.total', 'buckets');
$this->migrateUsageMetrics('files.$all.count.total', 'files');
$this->migrateUsageMetrics('files.$all.storage.size', 'files.storage');
// There is also project.$all.storage.size which is the same as files.$all.storage.size
foreach ($this->documentsIterator('buckets') as $bucket) {
$id = "bucket_{$bucket->getInternalId()}";
@@ -315,9 +492,55 @@ class V20 extends Migration
$bucketId = $bucket->getId();
$bucketInternalId = $bucket->getInternalId();
$this->migrateUsageMetrics("files.$bucketId.count.total", "$bucketInternalId.files");
$this->migrateUsageMetrics("files.$bucketId.count.total", "$bucketInternalId.files");
$this->migrateUsageMetrics("files.$bucketId.storage.size", "$bucketInternalId.files.storage");
// some stats come with $ prefix in front of the id -> files.$650c3fda307b7fec4934.storage.size;
}
}
/**
* Fix run on each document
*
* @param Document $document
* @return Document
*/
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'projects':
/**
* Bump version number.
*/
$document->setAttribute('version', '1.5.0');
break;
case 'users':
if ($document->getAttribute('email', '') !== '') {
$target = new Document([
'$id' => ID::unique(),
'userId' => $document->getId(),
'userInternalId' => $document->getInternalId(),
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $document->getAttribute('email'),
]);
$this->projectDB->createDocument('targets', $target);
}
if ($document->getAttribute('phone', '') !== '') {
$target = new Document([
'$id' => ID::unique(),
'userId' => $document->getId(),
'userInternalId' => $document->getInternalId(),
'providerType' => MESSAGE_TYPE_SMS,
'identifier' => $document->getAttribute('phone'),
]);
$this->projectDB->createDocument('targets', $target);
}
break;
case 'sessions':
$duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$document->setAttribute('expire', $expire);
break;
}
return $document;
}
}
@@ -269,7 +269,7 @@ class CalcTierStats extends Action
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats_v2', [
$requestDocs = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
@@ -163,8 +163,8 @@ class CreateInfMetric extends Action
try {
$id = \md5("_inf_{$metric}");
$dbForProject->deleteDocument('stats_v2', $id);
$dbForProject->createDocument('stats_v2', new Document([
$dbForProject->deleteDocument('stats', $id);
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'metric' => $metric,
'period' => 'inf',
@@ -186,7 +186,7 @@ class CreateInfMetric extends Action
protected function getFromMetric(database $dbForProject, string $metric): int|float
{
return $dbForProject->sum('stats_v2', 'value', [
return $dbForProject->sum('stats', 'value', [
Query::equal('metric', [
$metric,
]),
+15 -3
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
@@ -263,12 +264,23 @@ class Deletes extends Action
Query::equal('targetInternalId', [$target->getInternalId()])
],
$dbForProject,
function (Document $subscriber) use ($dbForProject) {
function (Document $subscriber) use ($dbForProject, $target) {
$topicId = $subscriber->getAttribute('topicId');
$topicInternalId = $subscriber->getAttribute('topicInternalId');
$topic = $dbForProject->getDocument('topics', $topicId);
if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) {
$dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', min: 0);
$totalAttribute = match ($target->getAttribute('providerType')) {
MESSAGE_TYPE_EMAIL => 'emailTotal',
MESSAGE_TYPE_SMS => 'smsTotal',
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'),
};
$dbForProject->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
min: 0
);
}
}
);
@@ -457,7 +469,7 @@ class Deletes extends Action
{
$dbForProject = $getProjectDB($project);
// Delete Usage stats
$this->deleteByGroup('stats_v2', [
$this->deleteByGroup('stats', [
Query::lessThan('time', $hourlyUsageRetentionDatetime),
Query::equal('period', ['1h']),
], $dbForProject);
+1 -1
View File
@@ -286,7 +286,7 @@ class Hamster extends Action
$limit = $periodValue['limit'];
$period = $periodValue['period'];
$requestDocs = $dbForProject->find('stats_v2', [
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
+1 -1
View File
@@ -249,7 +249,7 @@ class Messaging extends Action
}
// Deleting push targets when token has expired.
if (($result['error'] ?? '') === 'Expired device token.') {
if (($result['error'] ?? '') === 'Expired device token') {
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$result['recipient']])
]);
+13 -12
View File
@@ -69,6 +69,7 @@ class Usage extends Action
getProjectDB: $getProjectDB
);
}
self::$stats[$projectId]['project'] = $project;
foreach ($payload['metrics'] ?? [] as $metric) {
if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) {
@@ -105,8 +106,8 @@ class Usage extends Action
}
break;
case $document->getCollection() === 'databases': // databases
$collections = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS)));
$documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
$collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS)));
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
if (!empty($collections['value'])) {
$metrics[] = [
'key' => METRIC_COLLECTIONS,
@@ -124,7 +125,7 @@ class Usage extends Action
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS)));
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS)));
if (!empty($documents['value'])) {
$metrics[] = [
@@ -139,8 +140,8 @@ class Usage extends Action
break;
case $document->getCollection() === 'buckets':
$files = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES)));
$storage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
$files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES)));
$storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
if (!empty($files['value'])) {
$metrics[] = [
@@ -158,13 +159,13 @@ class Usage extends Action
break;
case $document->getCollection() === 'functions':
$deployments = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS)));
$deploymentsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE)));
$builds = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS)));
$buildsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE)));
$buildsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE)));
$executions = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS)));
$executionsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
$deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS)));
$deploymentsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE)));
$builds = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS)));
$buildsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE)));
$buildsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE)));
$executions = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS)));
$executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
if (!empty($deployments['value'])) {
$metrics[] = [
+3 -3
View File
@@ -67,7 +67,7 @@ class UsageHook extends Usage
$id = \md5("{$time}_{$period}_{$key}");
try {
$dbForProject->createDocument('stats_v2', new Document([
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
@@ -78,14 +78,14 @@ class UsageHook extends Usage
} catch (Duplicate $th) {
if ($value < 0) {
$dbForProject->decreaseDocumentAttribute(
'stats_v2',
'stats',
$id,
'value',
abs($value)
);
} else {
$dbForProject->increaseDocumentAttribute(
'stats_v2',
'stats',
$id,
'value',
$value
@@ -7,7 +7,9 @@ class Topics extends Base
public const ALLOWED_ATTRIBUTES = [
'name',
'description',
'total'
'emailTotal',
'smsTotal',
'pushTotal',
];
/**
+341
View File
@@ -0,0 +1,341 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
use Utopia\Database\Query;
class V17 extends Filter
{
protected const CHAR_SINGLE_QUOTE = '\'';
protected const CHAR_DOUBLE_QUOTE = '"';
protected const CHAR_COMMA = ',';
protected const CHAR_SPACE = ' ';
protected const CHAR_BRACKET_START = '[';
protected const CHAR_BRACKET_END = ']';
protected const CHAR_PARENTHESES_START = '(';
protected const CHAR_PARENTHESES_END = ')';
protected const CHAR_BACKSLASH = '\\';
// Convert 1.4 params to 1.5
public function parse(array $content, string $model): array
{
switch ($model) {
case 'account.updateRecovery':
unset($content['passwordAgain']);
break;
// Queries
case 'account.listIdentities':
case 'account.listLogs':
case 'databases.list':
case 'databases.listLogs':
case 'databases.listCollections':
case 'databases.listCollectionLogs':
case 'databases.listAttributes':
case 'databases.listIndexes':
case 'databases.listDocuments':
case 'databases.getDocument':
case 'databases.listDocumentLogs':
case 'functions.list':
case 'functions.listDeployments':
case 'functions.listExecutions':
case 'migrations.list':
case 'projects.list':
case 'proxy.listRules':
case 'storage.listBuckets':
case 'storage.listFiles':
case 'teams.list':
case 'teams.listMemberships':
case 'teams.listLogs':
case 'users.list':
case 'users.listLogs':
case 'users.listIdentities':
case 'vcs.listInstallations':
$content = $this->convertOldQueries($content);
break;
}
return $content;
}
private function convertOldQueries(array $content): array
{
$parsed = [];
foreach ($content['queries'] as $query) {
try {
$query = $this->parseQuery($query);
$parsed[] = json_encode(array_filter($query->toArray()));
} catch (\Throwable $th) {
throw new \Exception("Invalid query: {$query}", previous: $th);
}
}
$content['queries'] = $parsed;
return $content;
}
// 1.4 query parser
public function parseQuery(string $filter): Query
{
// Init empty vars we fill later
$method = '';
$params = [];
// Separate method from filter
$paramsStart = mb_strpos($filter, '(');
if ($paramsStart === false) {
throw new \Exception('Invalid query');
}
$method = mb_substr($filter, 0, $paramsStart);
// Separate params from filter
$paramsEnd = \strlen($filter) - 1; // -1 to ignore )
$parametersStart = $paramsStart + 1; // +1 to ignore (
// Check for deprecated query syntax
if (\str_contains($method, '.')) {
throw new \Exception('Invalid query method');
}
$currentParam = ""; // We build param here before pushing when it's ended
$currentArrayParam = []; // We build array param here before pushing when it's ended
$stack = []; // State for stack of parentheses
$stackCount = 0; // Length of stack array. Kept as variable to improve performance
$stringStackState = null; // State for string support
// Loop thorough all characters
for ($i = $parametersStart; $i < $paramsEnd; $i++) {
$char = $filter[$i];
$isStringStack = $stringStackState !== null;
$isArrayStack = !$isStringStack && $stackCount > 0;
if ($char === static::CHAR_BACKSLASH) {
if (!(static::isSpecialChar($filter[$i + 1]))) {
static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam);
}
static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam);
$i++;
continue;
}
// String support + escaping support
if (
(self::isQuote($char)) && // Must be string indicator
($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped;
) {
if ($isStringStack) {
// Dont mix-up string symbols. Only allow the same as on start
if ($char === $stringStackState) {
// End of string
$stringStackState = null;
}
// Either way, add symbol to builder
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
} else {
// Start of string
$stringStackState = $char;
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
}
continue;
}
// Array support
if (!($isStringStack)) {
if ($char === static::CHAR_BRACKET_START) {
// Start of array
$stack[] = $char;
$stackCount++;
continue;
} elseif ($char === static::CHAR_BRACKET_END) {
// End of array
\array_pop($stack);
$stackCount--;
if (strlen($currentParam)) {
$currentArrayParam[] = $currentParam;
}
$params[] = $currentArrayParam;
$currentArrayParam = [];
$currentParam = "";
continue;
} elseif ($char === static::CHAR_COMMA) { // Params separation support
// If in array stack, dont merge yet, just mark it in array param builder
if ($isArrayStack) {
$currentArrayParam[] = $currentParam;
$currentParam = "";
} else {
// Append from parap builder. Either value, or array
if (empty($currentArrayParam)) {
if (strlen($currentParam)) {
$params[] = $currentParam;
}
$currentParam = "";
}
}
continue;
}
}
// Value, not relevant to syntax
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
}
if (strlen($currentParam)) {
$params[] = $currentParam;
$currentParam = "";
}
$parsedParams = [];
foreach ($params as $param) {
// If array, parse each child separatelly
if (\is_array($param)) {
foreach ($param as $element) {
$arr[] = self::parseValue($element);
}
$parsedParams[] = $arr ?? [];
} else {
$parsedParams[] = self::parseValue($param);
}
}
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_NOT_EQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSER_EQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATER_EQUAL:
case Query::TYPE_CONTAINS:
case Query::TYPE_SEARCH:
case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
$attribute = $parsedParams[0] ?? '';
if (count($parsedParams) < 2) {
return new Query($method, $attribute);
}
return new Query($method, $attribute, \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]);
case Query::TYPE_BETWEEN:
return new Query($method, $parsedParams[0], [$parsedParams[1], $parsedParams[2]]);
case Query::TYPE_SELECT:
return new Query($method, values: $parsedParams[0]);
case Query::TYPE_ORDER_ASC:
case Query::TYPE_ORDER_DESC:
return new Query($method, $parsedParams[0] ?? '');
case Query::TYPE_LIMIT:
case Query::TYPE_OFFSET:
case Query::TYPE_CURSOR_AFTER:
case Query::TYPE_CURSOR_BEFORE:
if (count($parsedParams) > 0) {
return new Query($method, values: [$parsedParams[0]]);
}
return new Query($method);
default:
return new Query($method);
}
}
/**
* Parses value.
*
* @param string $value
* @return mixed
*/
private function parseValue(string $value): mixed
{
$value = \trim($value);
if ($value === 'false') { // Boolean value
return false;
} elseif ($value === 'true') {
return true;
} elseif ($value === 'null') { // Null value
return null;
} elseif (\is_numeric($value)) { // Numeric value
// Cast to number
return $value + 0;
} elseif (\str_starts_with($value, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($value, static::CHAR_SINGLE_QUOTE)) { // String param
$value = \substr($value, 1, -1); // Remove '' or ""
return $value;
}
// Unknown format
return $value;
}
/**
* Utility method to only append symbol if relevant.
*
* @param bool $isStringStack
* @param string $char
* @param int $index
* @param string $filter
* @param string $currentParam
* @return void
*/
private function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void
{
// Ignore spaces and commas outside of string
$canBeIgnored = false;
if ($char === static::CHAR_SPACE) {
$canBeIgnored = true;
} elseif ($char === static::CHAR_COMMA) {
$canBeIgnored = true;
}
if ($canBeIgnored) {
if ($isStringStack) {
$currentParam .= $char;
}
} else {
$currentParam .= $char;
}
}
private function isQuote(string $char): bool
{
if ($char === self::CHAR_SINGLE_QUOTE) {
return true;
} elseif ($char === self::CHAR_DOUBLE_QUOTE) {
return true;
}
return false;
}
private function isSpecialChar(string $char): bool
{
if ($char === static::CHAR_COMMA) {
return true;
} elseif ($char === static::CHAR_BRACKET_END) {
return true;
} elseif ($char === static::CHAR_BRACKET_START) {
return true;
} elseif ($char === static::CHAR_DOUBLE_QUOTE) {
return true;
} elseif ($char === static::CHAR_SINGLE_QUOTE) {
return true;
}
return false;
}
}
@@ -0,0 +1,48 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
class V17 extends Filter
{
// Convert 1.5 Data format to 1.4 format
public function parse(array $content, string $model): array
{
$parsedResponse = $content;
switch ($model) {
case Response::MODEL_PROJECT:
$parsedResponse = $this->parseProject($parsedResponse);
break;
case Response::MODEL_USER:
$parsedResponse = $this->parseUser($parsedResponse);
break;
case Response::MODEL_TOKEN:
$parsedResponse = $this->parseToken($parsedResponse);
break;
}
return $parsedResponse;
}
protected function parseUser(array $content)
{
unset($content['targets']);
return $content;
}
protected function parseProject(array $content)
{
$content['providers'] = $content['oAuthProviders'];
unset($content['oAuthProviders']);
return $content;
}
protected function parseToken(array $content)
{
unset($content['phrase']);
return $content;
}
}
+14 -2
View File
@@ -34,9 +34,21 @@ class Topic extends Model
'default' => '',
'example' => 'events',
])
->addRule('total', [
->addRule('emailTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total count of subscribers subscribed to topic.',
'description' => 'Total count of email subscribers subscribed to the topic.',
'default' => 0,
'example' => 100,
])
->addRule('smsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total count of SMS subscribers subscribed to the topic.',
'default' => 0,
'example' => 100,
])
->addRule('pushTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total count of push subscribers subscribed to the topic.',
'default' => 0,
'example' => 100,
])