1
0
Fork 0
mirror of synced 2024-06-14 08:44:49 +12:00

Merge branch 'feat-database-indexing' of https://github.com/appwrite/appwrite into feat-db-project-subqueries

This commit is contained in:
Torsten Dittmann 2021-10-06 13:56:06 +02:00
commit 97aa1908c8
71 changed files with 5544 additions and 750 deletions

View file

@ -6,6 +6,10 @@ arch:
os: linux
# Small change
vm:
size: large
language: shell
notifications:
@ -35,6 +39,12 @@ install:
script:
- docker ps -a
# Tests should fail if any container is in exited status
- ALL_UP=`docker ps -aq --filter "status=exited"`
- >
if [[ "$ALL_UP" != "" ]]; then
exit 1
fi
- docker-compose logs appwrite
- docker-compose logs mariadb
- docker-compose logs appwrite-worker-functions

View file

@ -225,6 +225,7 @@ RUN mkdir -p /storage/uploads && \
# Executables
RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/maintenance && \
chmod +x /usr/local/bin/usage && \
chmod +x /usr/local/bin/install && \
chmod +x /usr/local/bin/migrate && \
chmod +x /usr/local/bin/schedule && \

View file

@ -1,6 +1,6 @@
<?php
require_once __DIR__.'/workers.php';
require_once __DIR__.'/init.php';
use Utopia\App;
use Utopia\CLI\CLI;
@ -15,6 +15,7 @@ include 'tasks/migrate.php';
include 'tasks/sdks.php';
include 'tasks/ssl.php';
include 'tasks/vars.php';
include 'tasks/usage.php';
$cli
->task('version')

View file

@ -174,7 +174,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
'filters' => ['casting'],
],
[
'$id' => 'signed',
@ -214,7 +214,7 @@ $collections = [
'required' => false,
'default' => new stdClass,
'array' => false,
'filters' => ['json'],
'filters' => ['json', 'range'],
],
[
'$id' => 'filters',
@ -2082,6 +2082,91 @@ $collections = [
],
],
],
'stats' => [
'$collection' => Database::METADATA,
'$id' => 'stats',
'name' => 'Stats',
'attributes' => [
[
'$id' => 'metric',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'value',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'time',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'period',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 4,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'type',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 1,
'signed' => false,
'required' => true,
'default' => 0, // 0 -> count, 1 -> sum
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_time',
'type' => Database::INDEX_KEY,
'attributes' => ['time'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
[
'$id' => '_key_metric',
'type' => Database::INDEX_KEY,
'attributes' => ['metric'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_metric_period',
'type' => Database::INDEX_KEY,
'attributes' => ['metric', 'period'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
],
]
];
return $collections;

View file

@ -27,6 +27,21 @@ return [
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.email' => [
'description' => 'This event triggers when the user email address is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.name' => [
'description' => 'This event triggers when the user name is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.password' => [
'description' => 'This event triggers when the user password is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'account.update.prefs' => [
'description' => 'This event triggers when the account preferences are updated.',
'model' => Response::MODEL_USER,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -149,6 +149,15 @@ return [
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_USAGE_AGGREGATION_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds.',
'introduction' => '0.10.0',
'default' => '30',
'required' => false,
'question' => '',
'filter' => ''
]
],
],

View file

@ -52,12 +52,14 @@ App::post('/v1/account')
->inject('project')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $email, $password, $name, $request, $response, $project, $dbForInternal, $audits) {
->inject('usage')
->action(function ($userId, $email, $password, $name, $request, $response, $project, $dbForInternal, $audits, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$email = \strtolower($email);
if ('console' === $project->getId()) {
@ -117,9 +119,12 @@ App::post('/v1/account')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.create')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.create', 1)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
@ -147,13 +152,15 @@ App::post('/v1/account/sessions')
->inject('locale')
->inject('geodb')
->inject('audits')
->action(function ($email, $password, $request, $response, $dbForInternal, $locale, $geodb, $audits) {
->inject('usage')
->action(function ($email, $password, $request, $response, $dbForInternal, $locale, $geodb, $audits, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$email = \strtolower($email);
$protocol = $request->getProtocol();
@ -164,7 +171,7 @@ App::post('/v1/account/sessions')
$audits
//->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.failed')
->setParam('resource', 'users/'.($profile ? $profile->getId() : ''))
->setParam('resource', 'user/'.($profile ? $profile->getId() : ''))
;
throw new Exception('Invalid credentials', 401); // Wrong password or username
@ -205,7 +212,7 @@ App::post('/v1/account/sessions')
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'users/' . $profile->getId())
->setParam('resource', 'user/' . $profile->getId())
;
if (!Config::getParam('domainVerification')) {
@ -227,6 +234,11 @@ App::post('/v1/account/sessions')
->setAttribute('countryName', $countryName)
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.create', 1)
->setParam('provider', 'email')
;
$response->dynamic($session, Response::MODEL_SESSION);
});
@ -357,7 +369,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('geodb')
->inject('audits')
->inject('events')
->action(function ($provider, $code, $state, $request, $response, $project, $user, $dbForInternal, $geodb, $audits, $events) use ($oauthDefaultSuccess) {
->inject('usage')
->action(function ($provider, $code, $state, $request, $response, $project, $user, $dbForInternal, $geodb, $audits, $events, $usage) use ($oauthDefaultSuccess) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
@ -365,6 +378,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
/** @var Utopia\Database\Database $dbForInternal */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
@ -539,12 +553,17 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
->setParam('data', ['provider' => $provider])
;
$events->setParam('eventData', $response->output($session, Response::MODEL_SESSION));
$usage
->setParam('users.sessions.create', 1)
->setParam('projectId', $project->getId())
->setParam('provider', 'oauth2-'.$provider)
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
@ -595,7 +614,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('dbForInternal')
->inject('geodb')
->inject('audits')
->action(function ($request, $response, $locale, $user, $project, $dbForInternal, $geodb, $audits) {
->inject('usage')
->action(function ($request, $response, $locale, $user, $project, $dbForInternal, $geodb, $audits, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Locale\Locale $locale */
@ -604,6 +624,7 @@ App::post('/v1/account/sessions/anonymous')
/** @var Utopia\Database\Database $dbForInternal */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
@ -683,7 +704,12 @@ App::post('/v1/account/sessions/anonymous')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.sessions.create', 1)
->setParam('provider', 'anonymous')
;
if (!Config::getParam('domainVerification')) {
@ -771,10 +797,15 @@ App::get('/v1/account')
->label('sdk.response.model', Response::MODEL_USER)
->inject('response')
->inject('user')
->action(function ($response, $user) {
->inject('usage')
->action(function ($response, $user, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Stats\Stats $usage */
$usage
->setParam('users.read', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -791,12 +822,17 @@ App::get('/v1/account/prefs')
->label('sdk.response.model', Response::MODEL_PREFERENCES)
->inject('response')
->inject('user')
->action(function ($response, $user) {
->inject('usage')
->action(function ($response, $user, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Stats\Stats $usage */
$prefs = $user->getAttribute('prefs', new \stdClass());
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -814,10 +850,12 @@ App::get('/v1/account/sessions')
->inject('response')
->inject('user')
->inject('locale')
->action(function ($response, $user, $locale) {
->inject('usage')
->action(function ($response, $user, $locale, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Stats\Stats $usage */
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
@ -831,6 +869,9 @@ App::get('/v1/account/sessions')
$sessions[$key] = $session;
}
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document([
'sessions' => $sessions,
'sum' => count($sessions),
@ -853,13 +894,15 @@ App::get('/v1/account/logs')
->inject('locale')
->inject('geodb')
->inject('dbForInternal')
->action(function ($response, $user, $locale, $geodb, $dbForInternal) {
->inject('usage')
->action(function ($response, $user, $locale, $geodb, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$audit = new Audit($dbForInternal);
@ -906,6 +949,9 @@ App::get('/v1/account/logs')
}
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST);
});
@ -925,11 +971,13 @@ App::get('/v1/account/sessions/:sessionId')
->inject('user')
->inject('locale')
->inject('dbForInternal')
->action(function ($sessionId, $response, $user, $locale, $dbForInternal) {
->inject('usage')
->action(function ($sessionId, $response, $user, $locale, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$sessions = $user->getAttribute('sessions', []);
$sessionId = ($sessionId === 'current')
@ -948,6 +996,10 @@ App::get('/v1/account/sessions/:sessionId')
->setAttribute('countryName', $countryName)
;
$usage
->setParam('users.read', 1)
;
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
@ -972,18 +1024,24 @@ App::patch('/v1/account/name')
->inject('user')
->inject('dbForInternal')
->inject('audits')
->action(function ($name, $response, $user, $dbForInternal, $audits) {
->inject('usage')
->action(function ($name, $response, $user, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('name', $name));
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.name')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
@ -1007,11 +1065,13 @@ App::patch('/v1/account/password')
->inject('user')
->inject('dbForInternal')
->inject('audits')
->action(function ($password, $oldPassword, $response, $user, $dbForInternal, $audits) {
->inject('usage')
->action(function ($password, $oldPassword, $response, $user, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
@ -1026,9 +1086,12 @@ App::patch('/v1/account/password')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.password')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -1050,11 +1113,13 @@ App::patch('/v1/account/email')
->inject('user')
->inject('dbForInternal')
->inject('audits')
->action(function ($email, $password, $response, $user, $dbForInternal, $audits) {
->inject('usage')
->action(function ($email, $password, $response, $user, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
@ -1066,24 +1131,25 @@ App::patch('/v1/account/email')
}
$email = \strtolower($email);
$profile = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [\strtolower($email)])]); // Get user by email address
if ($profile) {
throw new Exception('User already registered', 400);
}
$user = $dbForInternal->updateDocument('users', $user->getId(), $user
try {
$user = $dbForInternal->updateDocument('users', $user->getId(), $user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
);
);
} catch(Duplicate $th) {
throw new Exception('Email already exists', 409);
}
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.email')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -1104,19 +1170,24 @@ App::patch('/v1/account/prefs')
->inject('user')
->inject('dbForInternal')
->inject('audits')
->action(function ($prefs, $response, $user, $dbForInternal, $audits) {
->inject('usage')
->action(function ($prefs, $response, $user, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$audits
->setParam('event', 'account.update.prefs')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -1137,13 +1208,15 @@ App::delete('/v1/account')
->inject('dbForInternal')
->inject('audits')
->inject('events')
->action(function ($request, $response, $user, $dbForInternal, $audits, $events) {
->inject('usage')
->action(function ($request, $response, $user, $dbForInternal, $audits, $events, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
@ -1159,7 +1232,7 @@ App::delete('/v1/account')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.delete')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
->setParam('data', $user->getArrayCopy())
;
@ -1173,6 +1246,9 @@ App::delete('/v1/account')
;
}
$usage
->setParam('users.delete', 1)
;
$response
->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
@ -1200,7 +1276,8 @@ App::delete('/v1/account/sessions/:sessionId')
->inject('locale')
->inject('audits')
->inject('events')
->action(function ($sessionId, $request, $response, $user, $dbForInternal, $locale, $audits, $events) {
->inject('usage')
->action(function ($sessionId, $request, $response, $user, $dbForInternal, $locale, $audits, $events, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
@ -1208,6 +1285,7 @@ App::delete('/v1/account/sessions/:sessionId')
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@ -1225,7 +1303,7 @@ App::delete('/v1/account/sessions/:sessionId')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.delete')
->setParam('resource', '/user/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$session->setAttribute('current', false);
@ -1254,6 +1332,10 @@ App::delete('/v1/account/sessions/:sessionId')
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
;
$usage
->setParam('users.sessions.delete', 1)
->setParam('users.update', 1)
;
return $response->noContent();
}
}
@ -1280,7 +1362,8 @@ App::delete('/v1/account/sessions')
->inject('locale')
->inject('audits')
->inject('events')
->action(function ($request, $response, $user, $dbForInternal, $locale, $audits, $events) {
->inject('usage')
->action(function ($request, $response, $user, $dbForInternal, $locale, $audits, $events, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
@ -1288,6 +1371,7 @@ App::delete('/v1/account/sessions')
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@ -1298,7 +1382,7 @@ App::delete('/v1/account/sessions')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.delete')
->setParam('resource', '/user/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
if (!Config::getParam('domainVerification')) {
@ -1323,13 +1407,19 @@ App::delete('/v1/account/sessions')
$dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('sessions', []));
$numOfSessions = count($sessions);
$events
->setParam('eventData', $response->output(new Document([
'sessions' => $sessions,
'sum' => count($sessions),
'sum' => $numOfSessions,
]), Response::MODEL_SESSION_LIST))
;
$usage
->setParam('users.sessions.delete', $numOfSessions)
->setParam('users.update', 1)
;
$response->noContent();
});
@ -1357,7 +1447,8 @@ App::post('/v1/account/recovery')
->inject('mails')
->inject('audits')
->inject('events')
->action(function ($email, $url, $request, $response, $dbForInternal, $project, $locale, $mails, $audits, $events) {
->inject('usage')
->action(function ($email, $url, $request, $response, $dbForInternal, $project, $locale, $mails, $audits, $events, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
@ -1366,6 +1457,7 @@ App::post('/v1/account/recovery')
/** @var Appwrite\Event\Event $mails */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);
@ -1430,9 +1522,12 @@ App::post('/v1/account/recovery')
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.recovery.create')
->setParam('resource', 'users/' . $profile->getId())
->setParam('resource', 'user/' . $profile->getId())
;
$usage
->setParam('users.update', 1)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($recovery, Response::MODEL_TOKEN);
});
@ -1458,10 +1553,12 @@ App::put('/v1/account/recovery')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $secret, $password, $passwordAgain, $response, $dbForInternal, $audits) {
->inject('usage')
->action(function ($userId, $secret, $password, $passwordAgain, $response, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
if ($password !== $passwordAgain) {
throw new Exception('Passwords must match', 400);
@ -1505,9 +1602,12 @@ App::put('/v1/account/recovery')
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.recovery.update')
->setParam('resource', 'users/' . $profile->getId())
->setParam('resource', 'user/' . $profile->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($recovery, Response::MODEL_TOKEN);
});
@ -1535,7 +1635,8 @@ App::post('/v1/account/verification')
->inject('audits')
->inject('events')
->inject('mails')
->action(function ($url, $request, $response, $project, $user, $dbForInternal, $locale, $audits, $events, $mails) {
->inject('usage')
->action(function ($url, $request, $response, $project, $user, $dbForInternal, $locale, $audits, $events, $mails, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
@ -1545,6 +1646,7 @@ App::post('/v1/account/verification')
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $mails */
/** @var Appwrite\Stats\Stats $usage */
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);
@ -1599,9 +1701,12 @@ App::post('/v1/account/verification')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.verification.create')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($verification, Response::MODEL_TOKEN);
});
@ -1626,11 +1731,13 @@ App::put('/v1/account/verification')
->inject('user')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $secret, $response, $user, $dbForInternal, $audits) {
->inject('usage')
->action(function ($userId, $secret, $response, $user, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$profile = $dbForInternal->getDocument('users', $userId);
@ -1665,8 +1772,11 @@ App::put('/v1/account/verification')
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.verification.update')
->setParam('resource', 'users/' . $user->getId())
->setParam('resource', 'user/' . $user->getId())
;
$usage
->setParam('users.update', 1)
;
$response->dynamic($verification, Response::MODEL_TOKEN);
});

File diff suppressed because it is too large Load diff

View file

@ -146,6 +146,9 @@ App::get('/v1/functions/:functionId/usage')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
->param('functionId', '', new UID(), 'Function unique ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
->inject('response')
@ -164,99 +167,62 @@ App::get('/v1/functions/:functionId/usage')
throw new Exception('Function not found', 404);
}
$usage = [];
if(App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
'24h' => [
'start' => DateTime::createFromFormat('U', \strtotime('-24 hours')),
'end' => DateTime::createFromFormat('U', \strtotime('+1 hour')),
'group' => '30m',
'period' => '30m',
'limit' => 48,
],
'7d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-7 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 7,
],
'30d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-30 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 30,
],
'90d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-90 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 90,
],
];
$metrics = [
"functions.$functionId.executions",
"functions.$functionId.failures",
"functions.$functionId.compute"
];
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
$client = $register->get('influxdb');
$executions = [];
$failures = [];
$compute = [];
if ($client) {
$start = $period[$range]['start']->format(DateTime::RFC3339);
$end = $period[$range]['end']->format(DateTime::RFC3339);
$database = $client->selectDB('telegraf');
// Executions
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$executions[] = [
'value' => (!empty($point['value'])) ? $point['value'] : 0,
'date' => \strtotime($point['time']),
];
}
// Failures
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' AND "functionStatus"=\'failed\' GROUP BY time('.$period[$range]['group'].') FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$failures[] = [
'value' => (!empty($point['value'])) ? $point['value'] : 0,
'date' => \strtotime($point['time']),
];
}
// Compute
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_time" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$compute[] = [
'value' => round((!empty($point['value'])) ? $point['value'] / 1000 : 0, 2), // minutes
'date' => \strtotime($point['time']),
];
}
}
$response->json([
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'executions' => [
'data' => $executions,
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $executions)),
],
'failures' => [
'data' => $failures,
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $failures)),
],
'compute' => [
'data' => $compute,
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $compute)),
],
'functions.executions' => $stats["functions.$functionId.executions"],
'functions.failures' => $stats["functions.$functionId.failures"],
'functions.compute' => $stats["functions.$functionId.compute"]
]);
} else {
$response->json([]);
}
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTIONS);
});
App::put('/v1/functions/:functionId')

View file

@ -6,11 +6,15 @@ use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\CLI;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Exception;
@ -20,13 +24,11 @@ use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\Audit\Audit;
use Utopia\Abuse\Adapters\TimeLimit;
App::init(function ($project) {
/** @var Utopia\Database\Document $project */
if($project->getId() !== 'console') {
if ($project->getId() !== 'console') {
throw new Exception('Access to this API is forbidden.', 401);
}
}, ['project'], 'projects');
@ -70,7 +72,7 @@ App::post('/v1/projects')
if ($team->isEmpty()) {
throw new Exception('Team not found', 404);
}
$auth = Config::getParam('auth', []);
$auths = ['limit' => 0];
foreach ($auth as $index => $method) {
@ -217,22 +219,25 @@ App::get('/v1/projects/:projectId')
});
App::get('/v1/projects/:projectId/usage')
->desc('Get Project Usage')
->desc('Get usage stats for a project')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForConsole')
->inject('projectDB')
->inject('dbForInternal')
->inject('register')
->action(function ($projectId, $range, $response, $dbForConsole, $projectDB, $register) {
->action(function ($projectId, $range, $response, $dbForConsole, $dbForInternal, $register) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Appwrite\Database\Database $projectDB */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Registry\Registry $register */
$project = $dbForConsole->getDocument('projects', $projectId);
@ -241,172 +246,72 @@ App::get('/v1/projects/:projectId/usage')
throw new Exception('Project not found', 404);
}
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
'24h' => [
'start' => DateTime::createFromFormat('U', \strtotime('-24 hours')),
'end' => DateTime::createFromFormat('U', \strtotime('+1 hour')),
'group' => '30m',
'period' => '30m',
'limit' => 48,
],
'7d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-7 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 7,
],
'30d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-30 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 30,
],
'90d' => [
'start' => DateTime::createFromFormat('U', \strtotime('-90 days')),
'end' => DateTime::createFromFormat('U', \strtotime('now')),
'group' => '1d',
'period' => '1d',
'limit' => 90,
],
];
$client = $register->get('influxdb');
$dbForInternal->setNamespace('project_' . $projectId . '_internal');
$requests = [];
$network = [];
$functions = [];
$metrics = [
'requests',
'network',
'executions',
'users.count',
'database.documents.count',
'database.collections.count',
'storage.total'
];
if ($client) {
$start = $period[$range]['start']->format(DateTime::RFC3339);
$end = $period[$range]['end']->format(DateTime::RFC3339);
$database = $client->selectDB('telegraf');
$stats = [];
// Requests
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_requests_all" WHERE time > \'' . $start . '\' AND time < \'' . $end . '\' AND "metric_type"=\'counter\' AND "project"=\'' . $project->getId() . '\' GROUP BY time(' . $period[$range]['group'] . ') FILL(null)');
$points = $result->getPoints();
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
foreach ($points as $point) {
$requests[] = [
'value' => (!empty($point['value'])) ? $point['value'] : 0,
'date' => \strtotime($point['time']),
];
}
// Network
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_network_all" WHERE time > \'' . $start . '\' AND time < \'' . $end . '\' AND "metric_type"=\'counter\' AND "project"=\'' . $project->getId() . '\' GROUP BY time(' . $period[$range]['group'] . ') FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$network[] = [
'value' => (!empty($point['value'])) ? $point['value'] : 0,
'date' => \strtotime($point['time']),
];
}
// Functions
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_all" WHERE time > \'' . $start . '\' AND time < \'' . $end . '\' AND "metric_type"=\'counter\' AND "project"=\'' . $project->getId() . '\' GROUP BY time(' . $period[$range]['group'] . ') FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$functions[] = [
'value' => (!empty($point['value'])) ? $point['value'] : 0,
'date' => \strtotime($point['time']),
];
}
}
} else {
$requests = [];
$network = [];
$functions = [];
}
// Users
$projectDB->getCollection([
'limit' => 0,
'offset' => 0,
'filters' => [
'$collection=users',
],
]);
$usersTotal = $projectDB->getSum();
// Documents
$collections = $projectDB->getCollection([
'limit' => 100,
'offset' => 0,
'filters' => [
'$collection=collections',
],
]);
$collectionsTotal = $projectDB->getSum();
$documents = [];
foreach ($collections as $collection) {
$result = $projectDB->getCollection([
'limit' => 0,
'offset' => 0,
'filters' => [
'$collection=' . $collection['$id'],
],
$usage = new Document([
'range' => $range,
'requests' => $stats['requests'],
'network' => $stats['network'],
'functions' => $stats['executions'],
'documents' => $stats['database.documents.count'],
'collections' => $stats['database.collections.count'],
'users' => $stats['users.count'],
'storage' => $stats['storage.total']
]);
$documents[] = ['name' => $collection['name'], 'total' => $projectDB->getSum()];
}
$response->json([
'range' => $range,
'requests' => [
'data' => $requests,
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $requests)),
],
'network' => [
'data' => \array_map(function ($value) {return ['value' => \round($value['value'] / 1000000, 2), 'date' => $value['date']];}, $network), // convert bytes to mb
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $network)),
],
'functions' => [
'data' => $functions,
'total' => \array_sum(\array_map(function ($item) {
return $item['value'];
}, $functions)),
],
'collections' => [
'data' => $collections,
'total' => $collectionsTotal,
],
'documents' => [
'data' => $documents,
'total' => \array_sum(\array_map(function ($item) {
return $item['total'];
}, $documents)),
],
'users' => [
'data' => [],
'total' => $usersTotal,
],
'storage' => [
'total' => $projectDB->getCount(
[
'attribute' => 'sizeOriginal',
'filters' => [
'$collection=files',
],
]
) +
$projectDB->getCount(
[
'attribute' => 'size',
'filters' => [
'$collection=tags',
],
]
),
],
]);
$response->dynamic($usage, Response::MODEL_USAGE_PROJECT);
});
App::patch('/v1/projects/:projectId')
@ -469,7 +374,7 @@ App::patch('/v1/projects/:projectId/service')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), function($element) {return $element['optional'];})), true), 'Service name.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), function ($element) {return $element['optional'];})), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForConsole')

View file

@ -10,6 +10,7 @@ use Utopia\Validator\HexColor;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Filesystem;
use Appwrite\ClamAV\Network;
use Utopia\Database\Validator\Authorization;
use Appwrite\Database\Validator\CustomId;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
@ -22,6 +23,7 @@ use Utopia\Image\Image;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
App::post('/v1/storage/files')
@ -54,7 +56,7 @@ App::post('/v1/storage/files')
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $usage */
/** @var Appwrite\Stats\Stats $usage */
$file = $request->getFiles('file');
@ -145,11 +147,13 @@ App::post('/v1/storage/files')
$audits
->setParam('event', 'storage.files.create')
->setParam('resource', 'storage/files/'.$file->getId())
->setParam('resource', 'file/'.$file->getId())
;
$usage
->setParam('storage', $sizeActual)
->setParam('storage.files.create', 1)
->setParam('bucketId', 'default')
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -175,9 +179,11 @@ App::get('/v1/storage/files')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($search, $limit, $offset, $after, $orderType, $response, $dbForInternal) {
->inject('usage')
->action(function ($search, $limit, $offset, $after, $orderType, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, $search)] : [];
@ -189,6 +195,11 @@ App::get('/v1/storage/files')
}
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')
;
$response->dynamic(new Document([
'files' => $dbForInternal->find('files', $queries, $limit, $offset, [], [$orderType], $afterFile ?? null),
'sum' => $dbForInternal->count('files', $queries, APP_LIMIT_COUNT),
@ -209,16 +220,21 @@ App::get('/v1/storage/files/:fileId')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($fileId, $response, $dbForInternal) {
->inject('usage')
->action(function ($fileId, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$file = $dbForInternal->getDocument('files', $fileId);
if (empty($file->getId())) {
throw new Exception('File not found', 404);
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')
;
$response->dynamic($file, Response::MODEL_FILE);
});
@ -249,11 +265,13 @@ App::get('/v1/storage/files/:fileId/preview')
->inject('response')
->inject('project')
->inject('dbForInternal')
->action(function ($fileId, $width, $height, $gravity, $quality, $borderWidth, $borderColor, $borderRadius, $opacity, $rotation, $background, $output, $request, $response, $project, $dbForInternal) {
->inject('usage')
->action(function ($fileId, $width, $height, $gravity, $quality, $borderWidth, $borderColor, $borderRadius, $opacity, $rotation, $background, $output, $request, $response, $project, $dbForInternal, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $stats */
$storage = 'files';
@ -366,6 +384,11 @@ App::get('/v1/storage/files/:fileId/preview')
$cache->save($key, $data);
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')
;
$response
->setContentType($outputs[$output])
->addHeader('Expires', $date)
@ -390,9 +413,11 @@ App::get('/v1/storage/files/:fileId/download')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($fileId, $response, $dbForInternal) {
->inject('usage')
->action(function ($fileId, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$file = $dbForInternal->getDocument('files', $fileId);
@ -424,6 +449,11 @@ App::get('/v1/storage/files/:fileId/download')
$source = $compressor->decompress($source);
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')
;
// Response
$response
->setContentType($file->getAttribute('mimeType'))
@ -448,9 +478,11 @@ App::get('/v1/storage/files/:fileId/view')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($fileId, $response, $dbForInternal) {
->inject('usage')
->action(function ($fileId, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$file = $dbForInternal->getDocument('files', $fileId);
$mimes = Config::getParam('storage-mimes');
@ -490,6 +522,11 @@ App::get('/v1/storage/files/:fileId/view')
$output = $compressor->decompress($source);
$fileName = $file->getAttribute('name', '');
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')
;
// Response
$response
->setContentType($contentType)
@ -520,7 +557,8 @@ App::put('/v1/storage/files/:fileId')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($fileId, $read, $write, $response, $dbForInternal, $audits) {
->inject('usage')
->action(function ($fileId, $read, $write, $response, $dbForInternal, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
@ -539,7 +577,12 @@ App::put('/v1/storage/files/:fileId')
$audits
->setParam('event', 'storage.files.update')
->setParam('resource', 'storage/files/'.$file->getId())
->setParam('resource', 'file/'.$file->getId())
;
$usage
->setParam('storage.files.update', 1)
->setParam('bucketId', 'default')
;
$response->dynamic($file, Response::MODEL_FILE);
@ -567,7 +610,7 @@ App::delete('/v1/storage/files/:fileId')
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $usage */
/** @var Appwrite\Stats\Stats $usage */
$file = $dbForInternal->getDocument('files', $fileId);
@ -585,11 +628,13 @@ App::delete('/v1/storage/files/:fileId')
$audits
->setParam('event', 'storage.files.delete')
->setParam('resource', 'storage/files/'.$file->getId())
->setParam('resource', 'file/'.$file->getId())
;
$usage
->setParam('storage', $file->getAttribute('size', 0) * -1)
->setParam('storage.files.delete', 1)
->setParam('bucketId', 'default')
;
$events
@ -597,4 +642,159 @@ App::delete('/v1/storage/files/:fileId')
;
$response->noContent();
});
App::get('/v1/storage/usage')
->desc('Get usage stats for storage')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_STORAGE)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($range, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
'24h' => [
'period' => '30m',
'limit' => 48,
],
'7d' => [
'period' => '1d',
'limit' => 7,
],
'30d' => [
'period' => '1d',
'limit' => 30,
],
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$metrics = [
"storage.total",
"storage.files.count"
];
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'storage' => $stats['storage.total'],
'files' => $stats['storage.files.count']
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_STORAGE);
});
App::get('/v1/storage/:bucketId/usage')
->desc('Get usage stats for a storage bucket')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getBucketUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_BUCKETS)
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($bucketId, $range, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
// TODO: Check if the storage bucket exists else throw 404
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
'24h' => [
'period' => '30m',
'limit' => 48,
],
'7d' => [
'period' => '1d',
'limit' => 7,
],
'30d' => [
'period' => '1d',
'limit' => 30,
],
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$metrics = [
"storage.buckets.$bucketId.files.count",
"storage.buckets.$bucketId.files.create",
"storage.buckets.$bucketId.files.read",
"storage.buckets.$bucketId.files.update",
"storage.buckets.$bucketId.files.delete"
];
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'files.count' => $stats["storage.buckets.$bucketId.files.count"],
'files.create' => $stats["storage.buckets.$bucketId.files.create"],
'files.read' => $stats["storage.buckets.$bucketId.files.read"],
'files.update' => $stats["storage.buckets.$bucketId.files.update"],
'files.delete' => $stats["storage.buckets.$bucketId.files.delete"]
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_BUCKETS);
});

View file

@ -399,7 +399,7 @@ App::post('/v1/teams/:teamId/memberships')
$audits
->setParam('userId', $invitee->getId())
->setParam('event', 'teams.memberships.create')
->setParam('resource', 'teams/'.$teamId)
->setParam('resource', 'team/'.$teamId)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -561,7 +561,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'teams.memberships.update')
->setParam('resource', 'teams/'.$teamId)
->setParam('resource', 'team/'.$teamId)
;
$response->dynamic($membership, Response::MODEL_MEMBERSHIP);
@ -686,7 +686,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'teams.memberships.update.status')
->setParam('resource', 'teams/'.$teamId)
->setParam('resource', 'team/'.$teamId)
;
if (!Config::getParam('domainVerification')) {
@ -779,7 +779,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$audits
->setParam('userId', $membership->getAttribute('userId'))
->setParam('event', 'teams.memberships.delete')
->setParam('resource', 'teams/'.$teamId)
->setParam('resource', 'team/'.$teamId)
;
$events

View file

@ -17,6 +17,10 @@ use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\UID;
use DeviceDetector\DeviceDetector;
use Appwrite\Database\Validator\CustomId;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
App::post('/v1/users')
->desc('Create User')
@ -36,9 +40,11 @@ App::post('/v1/users')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $email, $password, $name, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $email, $password, $name, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$email = \strtolower($email);
@ -65,6 +71,10 @@ App::post('/v1/users')
throw new Exception('Account already exists', 409);
}
$usage
->setParam('users.create', 1)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
@ -87,9 +97,11 @@ App::get('/v1/users')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($search, $limit, $offset, $after, $orderType, $response, $dbForInternal) {
->inject('usage')
->action(function ($search, $limit, $offset, $after, $orderType, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
if (!empty($after)) {
$afterUser = $dbForInternal->getDocument('users', $after);
@ -101,6 +113,10 @@ App::get('/v1/users')
$results = $dbForInternal->find('users', [], $limit, $offset, [], [$orderType], $afterUser ?? null);
$sum = $dbForInternal->count('users', [], APP_LIMIT_COUNT);
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document([
'users' => $results,
@ -122,9 +138,11 @@ App::get('/v1/users/:userId')
->param('userId', '', new UID(), 'User unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -132,6 +150,9 @@ App::get('/v1/users/:userId')
throw new Exception('User not found', 404);
}
$usage
->setParam('users.read', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -149,9 +170,11 @@ App::get('/v1/users/:userId/prefs')
->param('userId', '', new UID(), 'User unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -161,6 +184,9 @@ App::get('/v1/users/:userId/prefs')
$prefs = $user->getAttribute('prefs', new \stdClass());
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -179,10 +205,12 @@ App::get('/v1/users/:userId/sessions')
->inject('response')
->inject('dbForInternal')
->inject('locale')
->action(function ($userId, $response, $dbForInternal, $locale) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $locale, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -202,6 +230,9 @@ App::get('/v1/users/:userId/sessions')
$sessions[$key] = $session;
}
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document([
'sessions' => $sessions,
'sum' => count($sessions),
@ -224,12 +255,14 @@ App::get('/v1/users/:userId/logs')
->inject('dbForInternal')
->inject('locale')
->inject('geodb')
->action(function ($userId, $response, $dbForInternal, $locale, $geodb) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $locale, $geodb, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -311,6 +344,9 @@ App::get('/v1/users/:userId/logs')
}
}
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST);
});
@ -330,9 +366,11 @@ App::patch('/v1/users/:userId/status')
->param('status', null, new Boolean(true), 'User Status. To activate the user pass `true` and to block the user pass `false`')
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $status, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $status, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -342,6 +380,9 @@ App::patch('/v1/users/:userId/status')
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -361,9 +402,11 @@ App::patch('/v1/users/:userId/verification')
->param('emailVerification', false, new Boolean(), 'User Email Verification Status.')
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $emailVerification, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $emailVerification, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -373,6 +416,132 @@ App::patch('/v1/users/:userId/verification')
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$usage
->setParam('users.update', 1)
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/name')
->desc('Update Name')
->groups(['api', 'users'])
->label('event', 'users.update.name')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateName')
->label('sdk.description', '/docs/references/users/update-user-name.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User unique ID.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $name, $response, $dbForInternal, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404);
}
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('name', $name));
$audits
->setParam('userId', $user->getId())
->setParam('event', 'users.update.name')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/password')
->desc('Update Password')
->groups(['api', 'users'])
->label('event', 'users.update.password')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePassword')
->label('sdk.description', '/docs/references/users/update-user-password.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User unique ID.')
->param('password', '', new Password(), 'New user password. Must be between 6 to 32 chars.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $password, $response, $dbForInternal, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404);
}
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('password', Auth::passwordHash($password))
->setAttribute('passwordUpdate', \time()));
$audits
->setParam('userId', $user->getId())
->setParam('event', 'users.update.password')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/email')
->desc('Update Email')
->groups(['api', 'users'])
->label('event', 'users.update.email')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmail')
->label('sdk.description', '/docs/references/users/update-user-email.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User unique ID.')
->param('email', '', new Email(), 'User email.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($userId, $email, $response, $dbForInternal, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404);
}
$email = \strtolower($email);
try {
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('email', $email));
} catch(Duplicate $th) {
throw new Exception('Email already exists', 409);
}
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.email')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
@ -392,9 +561,11 @@ App::patch('/v1/users/:userId/prefs')
->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('dbForInternal')
->action(function ($userId, $prefs, $response, $dbForInternal) {
->inject('usage')
->action(function ($userId, $prefs, $response, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -404,6 +575,9 @@ App::patch('/v1/users/:userId/prefs')
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage
->setParam('users.update', 1)
;
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -423,10 +597,12 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->inject('response')
->inject('dbForInternal')
->inject('events')
->action(function ($userId, $sessionId, $response, $dbForInternal, $events) {
->inject('usage')
->action(function ($userId, $sessionId, $response, $dbForInternal, $events, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -453,6 +629,11 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
}
}
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$response->noContent();
});
@ -471,10 +652,12 @@ App::delete('/v1/users/:userId/sessions')
->inject('response')
->inject('dbForInternal')
->inject('events')
->action(function ($userId, $response, $dbForInternal, $events) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $events, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -494,6 +677,10 @@ App::delete('/v1/users/:userId/sessions')
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$response->noContent();
});
@ -513,11 +700,13 @@ App::delete('/v1/users/:userId')
->inject('dbForInternal')
->inject('events')
->inject('deletes')
->action(function ($userId, $response, $dbForInternal, $events, $deletes) {
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $events, $deletes, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->getDocument('users', $userId);
@ -528,11 +717,6 @@ App::delete('/v1/users/:userId')
if (!$dbForInternal->deleteDocument('users', $userId)) {
throw new Exception('Failed to remove user from DB', 500);
}
// $dbForInternal->createDocument('users', new Document([
// '$id' => $userId,
// '$read' => ['role:all'],
// ]));
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
@ -543,5 +727,96 @@ App::delete('/v1/users/:userId')
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
$usage
->setParam('users.delete', 1)
;
$response->noContent();
});
App::get('/v1/users/usage')
->desc('Get usage stats for the users API')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(function($value) { return "oauth-".$value; }, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForInternal')
->inject('register')
->action(function ($range, $provider, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
'24h' => [
'period' => '30m',
'limit' => 48,
],
'7d' => [
'period' => '1d',
'limit' => 7,
],
'30d' => [
'period' => '1d',
'limit' => 30,
],
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$metrics = [
"users.count",
"users.create",
"users.read",
"users.update",
"users.delete",
"users.sessions.create",
"users.sessions.$provider.create",
"users.sessions.delete"
];
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'users.count' => $stats["users.count"],
'users.create' => $stats["users.create"],
'users.read' => $stats["users.read"],
'users.update' => $stats["users.update"],
'users.delete' => $stats["users.delete"],
'sessions.create' => $stats["users.sessions.create"],
'sessions.provider.create' => $stats["users.sessions.$provider.create"],
'sessions.delete' => $stats["users.sessions.delete"]
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_USERS);
});

View file

@ -104,6 +104,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
->setParam('httpRequest', 1)
->setParam('httpUrl', $request->getHostname().$request->getURI())
->setParam('httpMethod', $request->getMethod())
->setParam('httpPath', $route->getPath())
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->setParam('storage', 0)

View file

@ -387,11 +387,10 @@ App::get('/specs/:format')
}
$routes[] = $route;
$model = $response->getModel($route->getLabel('sdk.response.model', 'none'));
if($model) {
$models[$model->getType()] = $model;
}
$modelLabel = $route->getLabel('sdk.response.model', 'none');
$model = \is_array($modelLabel) ? \array_map(function($m) use($response) {
return $response->getModel($m);
}, $modelLabel) : $response->getModel($modelLabel);
}
}

View file

@ -17,6 +17,7 @@ ini_set('display_startup_errors', 1);
ini_set('default_socket_timeout', -1);
error_reporting(E_ALL);
use Appwrite\Extend\PDO;
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
@ -63,6 +64,11 @@ const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_CACHE_BUSTER = 151;
const APP_VERSION_STABLE = '0.9.4';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_CACHE = '/storage/cache';
@ -89,6 +95,7 @@ const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_CERTIFICATES = 'certificates';
const DELETE_TYPE_USAGE = 'usage';
// Mail Worker Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_RECOVERY = 'recovery';
@ -141,9 +148,7 @@ if(!empty($user) || !empty($pass)) {
}
/**
* DB Filters
*
* Make sure the value of an attribute that uses sub-query filters is set to 'null' otherwise the filters might not work properly
* Old DB Filters
*/
DatabaseOld::addFilter('json',
function($value) {
@ -179,6 +184,43 @@ DatabaseOld::addFilter('encrypt',
}
);
/**
* New DB Filters
*/
Database::addFilter('casting',
function($value) {
return json_encode(['value' => $value]);
},
function($value) {
if (is_null($value)) {
return null;
}
return json_decode($value, true)['value'];
}
);
Database::addFilter('range',
function($value, Document $attribute) {
if ($attribute->isSet('min')) {
$attribute->removeAttribute('min');
}
if ($attribute->isSet('max')) {
$attribute->removeAttribute('max');
}
return $value;
},
function($value, Document $attribute) {
$formatOptions = json_decode($attribute->getAttribute('formatOptions', []), true);
if (isset($formatOptions['min']) || isset($formatOptions['max'])) {
$attribute
->setAttribute('min', $formatOptions['min'])
->setAttribute('max', $formatOptions['max'])
;
}
return $value;
}
);
Database::addFilter('subQueryAttributes',
function($value) {
return null;
@ -187,7 +229,7 @@ Database::addFilter('subQueryAttributes',
return $database
->find('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], 1017, 0, []);
], $database->getAttributeLimit(), 0, []);
}
);
@ -211,7 +253,7 @@ Database::addFilter('subQueryPlatforms',
return $database
->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], 5000, 0, []);
], $database->getIndexLimit(), 0, []);
}
);
@ -223,7 +265,7 @@ Database::addFilter('subQueryDomains',
return $database
->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], 5000, 0, []);
], $database->getIndexLimit(), 0, []);
}
);
@ -235,7 +277,7 @@ Database::addFilter('subQueryKeys',
return $database
->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], 5000, 0, []);
], $database->getIndexLimit(), 0, []);
}
);
@ -247,7 +289,7 @@ Database::addFilter('subQueryWebhooks',
return $database
->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], 5000, 0, []);
], $database->getIndexLimit(), 0, []);
}
);
@ -275,25 +317,25 @@ Database::addFilter('encrypt',
/**
* DB Formats
*/
Structure::addFormat('email', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function() {
return new Email();
}, Database::VAR_STRING);
Structure::addFormat('ip', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function() {
return new IP();
}, Database::VAR_STRING);
Structure::addFormat('url', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function() {
return new URL();
}, Database::VAR_STRING);
Structure::addFormat('int-range', function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
Structure::addFormat('float-range', function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_FLOAT);
@ -396,6 +438,29 @@ $register->set('smtp', function () {
$register->set('geodb', function () {
return new Reader(__DIR__.'/db/DBIP/dbip-country-lite-2021-06.mmdb');
});
$register->set('db', function () { // This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
$dbPass = App::getEnv('_APP_DB_PASS', '');
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
));
return $pdo;
});
$register->set('cache', function () { // This is usually for our workers or CLI commands scope
$redis = new Redis();
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
/*
* Localization

View file

@ -39,17 +39,29 @@ $cli
]);
}
function notifyDeleteUsageStats(int $interval30m, int $interval1d)
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_USAGE,
'timestamp1d' => time() - $interval1d,
'timestamp30m' => time() - $interval30m,
]);
}
// # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
$auditLogRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', '1209600');
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600');//36 hours
$usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days
Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention){
Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying deletes workers every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
}, $interval);
});

586
app/tasks/usage.php Normal file
View file

@ -0,0 +1,586 @@
<?php
global $cli, $register;
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Cache\Adapter\Redis;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
/**
* Metrics We collect
*
* General
*
* requests
* network
* executions
*
* Database
*
* database.collections.create
* database.collections.read
* database.collections.update
* database.collections.delete
* database.documents.create
* database.documents.read
* database.documents.update
* database.documents.delete
* database.collections.{collectionId}.documents.create
* database.collections.{collectionId}.documents.read
* database.collections.{collectionId}.documents.update
* database.collections.{collectionId}.documents.delete
*
* Storage
*
* storage.buckets.{bucketId}.files.create
* storage.buckets.{bucketId}.files.read
* storage.buckets.{bucketId}.files.update
* storage.buckets.{bucketId}.files.delete
*
* Users
*
* users.create
* users.read
* users.update
* users.delete
* users.sessions.create
* users.sessions.{provider}.create
* users.sessions.delete
*
* Functions
*
* functions.{functionId}.executions
* functions.{functionId}.failures
* functions.{functionId}.compute
*
* Counters
*
* users.count
* storage.files.count
* database.collections.count
* database.documents.count
* database.collections.{collectionId}.documents.count
*
* Totals
*
* storage.total
*
*/
$cli
->task('usage')
->desc('Schedules syncing data from influxdb to Appwrite console db')
->action(function () use ($register) {
Console::title('Usage Aggregation V1');
Console::success(APP_NAME . ' usage aggregation process v1 has started');
$interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '30'); // 30 seconds (by default)
$periods = [
[
'key' => '30m',
'startTime' => '-24 hours',
],
[
'key' => '1d',
'startTime' => '-90 days',
],
];
// all the metrics that we are collecting at the moment
$globalMetrics = [
'requests' => [
'table' => 'appwrite_usage_requests_all',
],
'network' => [
'table' => 'appwrite_usage_network_all',
],
'executions' => [
'table' => 'appwrite_usage_executions_all',
],
'database.collections.create' => [
'table' => 'appwrite_usage_database_collections_create',
],
'database.collections.read' => [
'table' => 'appwrite_usage_database_collections_read',
],
'database.collections.update' => [
'table' => 'appwrite_usage_database_collections_update',
],
'database.collections.delete' => [
'table' => 'appwrite_usage_database_collections_delete',
],
'database.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
],
'database.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
],
'database.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
],
'database.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
],
'database.collections.collectionId.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
'groupBy' => 'collectionId',
],
'storage.buckets.bucketId.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
'groupBy' => 'bucketId',
],
'users.create' => [
'table' => 'appwrite_usage_users_create',
],
'users.read' => [
'table' => 'appwrite_usage_users_read',
],
'users.update' => [
'table' => 'appwrite_usage_users_update',
],
'users.delete' => [
'table' => 'appwrite_usage_users_delete',
],
'users.sessions.create' => [
'table' => 'appwrite_usage_users_sessions_create',
],
'users.sessions.provider.create' => [
'table' => 'appwrite_usage_users_sessions_create',
'groupBy' => 'provider',
],
'users.sessions.delete' => [
'table' => 'appwrite_usage_users_sessions_delete',
],
'functions.functionId.executions' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
],
'functions.functionId.compute' => [
'table' => 'appwrite_usage_executions_time',
'groupBy' => 'functionId',
],
'functions.functionId.failures' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
'filters' => [
'functionStatus' => 'failed',
],
],
];
// TODO Maybe move this to the setResource method, and reuse in the http.php file
$attempts = 0;
$max = 10;
$sleep = 1;
do { // connect to db
try {
$attempts++;
$db = $register->get('db');
$redis = $register->get('cache');
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
// TODO use inject
$cacheAdapter = new Cache(new Redis($redis));
$dbForProject = new Database(new MariaDB($db), $cacheAdapter);
$dbForConsole = new Database(new MariaDB($db), $cacheAdapter);
$dbForConsole->setNamespace('project_console_internal');
$latestTime = [];
Authorization::disable();
$iterations = 0;
Console::loop(function () use ($interval, $register, $dbForProject, $dbForConsole, $globalMetrics, $periods, &$latestTime, &$iterations) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating usage data every {$interval} seconds");
$loopStart = microtime(true);
/**
* Aggregate InfluxDB every 30 seconds
* @var InfluxDB\Client $client
*/
$client = $register->get('influxdb');
if ($client) {
$attempts = 0;
$max = 10;
$sleep = 1;
$database = $client->selectDB('telegraf');
do { // check if telegraf database is ready
$attempts++;
if(!in_array('telegraf', $client->listDatabases())) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
}
sleep($sleep);
} else {
break; // leave the do-while if successful
}
} while ($attempts < $max);
// sync data
foreach ($globalMetrics as $metric => $options) { //for each metrics
foreach ($periods as $period) { // aggregate data for each period
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $latestTime[$metric][$period['key']])->format(DateTime::RFC3339);
}
$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
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(function ($filter, $value) {
return '"' . $filter . '"=\'' . $value . '\'';
}, array_keys($filters), array_values($filters)));
}
$result = $database->query('SELECT sum(value) AS "value" FROM "' . $table . '" WHERE time > \'' . $start . '\' AND time < \'' . $end . '\' AND "metric_type"=\'counter\'' . (empty($filters) ? '' : $filters) . ' GROUP BY time(' . $period['key'] . '), "projectId"' . $groupBy . ' FILL(null)');
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId != 'console') {
$dbForProject->setNamespace('project_' . $projectId . '_internal');
$metricUpdated = $metric;
if (!empty($groupBy)) {
$groupedBy = $point[$options['groupBy']] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($options['groupBy'], $groupedBy, $metric);
}
$time = \strtotime($point['time']);
$id = \md5($time . '_' . $period['key'] . '_' . $metricUpdated); //Construct unique id for each metric using time, period and metric
$value = (!empty($point['value'])) ? $point['value'] : 0;
try {
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => $period['key'],
'time' => $time,
'metric' => $metricUpdated,
'value' => $value,
'type' => 0,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $value));
}
$latestTime[$metric][$period['key']] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
Console::warning("Failed to save data for project {$projectId} and metric {$metricUpdated}: {$e->getMessage()}");
}
}
}
}
}
}
/**
* Aggregate MariaDB every 15 minutes
* Some of the queries here might contain full-table scans.
*/
if ($iterations % 30 == 0) { // Every 15 minutes aggregate number of objects in database
$latestProject = null;
do { // Loop over all the projects
$attempts = 0;
$max = 10;
$sleep = 1;
do { // list projects
try {
$attempts++;
$projects = $dbForConsole->find('projects', [], 100, cursor:$latestProject);
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Console DB not ready yet. Retrying ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed access console db: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
if (empty($projects)) {
continue;
}
$latestProject = $projects[array_key_last($projects)];
foreach ($projects as $project) {
$projectId = $project->getId();
// Get total storage
$dbForProject->setNamespace('project_' . $projectId . '_internal');
$storageTotal = $dbForProject->sum('files', 'sizeOriginal') + $dbForProject->sum('tags', 'size');
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_storage.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '30m',
'time' => $time,
'metric' => 'storage.total',
'value' => $storageTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $storageTotal));
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_storage.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '1d',
'time' => $time,
'metric' => 'storage.total',
'value' => $storageTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $storageTotal));
}
$collections = [
'users' => [
'namespace' => 'internal',
],
'collections' => [
'metricPrefix' => 'database',
'namespace' => 'internal',
'subCollections' => [ // Some collections, like collections and later buckets have child collections that need counting
'documents' => [
'namespace' => 'external',
],
],
],
'files' => [
'metricPrefix' => 'storage',
'namespace' => 'internal',
],
];
foreach ($collections as $collection => $options) {
try {
$dbForProject->setNamespace("project_{$projectId}_{$options['namespace']}");
$count = $dbForProject->count($collection);
$dbForProject->setNamespace("project_{$projectId}_internal");
$metricPrefix = $options['metricPrefix'] ?? '';
$metric = empty($metricPrefix) ? "{$collection}.count" : "{$metricPrefix}.{$collection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
$subCollections = $options['subCollections'] ?? [];
if (empty($subCollections)) {
continue;
}
$latestParent = null;
$subCollectionCounts = []; //total project level count of sub collections
do { // Loop over all the parent collection document for each sub collection
$dbForProject->setNamespace("project_{$projectId}_{$options['namespace']}");
$parents = $dbForProject->find($collection, [], 100, cursor:$latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
if (empty($parents)) {
continue;
}
$latestParent = $parents[array_key_last($parents)];
foreach ($parents as $parent) {
foreach ($subCollections as $subCollection => $subOptions) { // Sub collection counts, like database.collections.collectionId.documents.count
$dbForProject->setNamespace("project_{$projectId}_{$subOptions['namespace']}");
$count = $dbForProject->count($parent->getId());
$subCollectionCounts[$subCollection] = ($subCollectionCounts[$subCollection] ?? 0) + $count; // Project level counts for sub collections like database.documents.count
$dbForProject->setNamespace("project_{$projectId}_internal");
$metric = empty($metricPrefix) ? "{$collection}.{$parent->getId()}.{$subCollection}.count" : "{$metricPrefix}.{$collection}.{$parent->getId()}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
}
}
} while (!empty($parents));
/**
* Inserting project level counts for sub collections like database.documents.count
*/
foreach ($subCollectionCounts as $subCollection => $count) {
$dbForProject->setNamespace("project_{$projectId}_internal");
$metric = empty($metricPrefix) ? "{$subCollection}.count" : "{$metricPrefix}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
}
}
} catch (\Exception$e) {
Console::warning("Failed to save database counters data for project {$collection}: {$e->getMessage()}");
}
}
}
} while (!empty($projects));
}
$iterations++;
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
});

View file

@ -17,6 +17,108 @@
</h1>
</div>
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-name">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>Update name</h2>
<form name="users.updateName"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update User Name"
data-service="users.updateName"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User name was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user name"
data-failure-param-alert-classname="error">
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<label for="name">Name</label>
<input name="name" id="name" type="text" autocomplete="off" data-ls-bind="{{user.name}}" required class="full-width" maxlength="128">
<hr />
<footer>
<button type="submit" class="">Update</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-email">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>Update Email</h2>
<form name="users.updateEmail"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update User Email"
data-service="users.updateEmail"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User email was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user email"
data-failure-param-alert-classname="error">
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<label for="email">Email</label>
<input name="email" id="email" type="text" autocomplete="off" data-ls-bind="{{user.email}}" required class="full-width" maxlength="128">
<hr />
<footer>
<button type="submit" class="">Update</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-password">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>Update Password</h2>
<form name="users.updatePassword"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update User Password"
data-service="users.updatePassword"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User password was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user password"
data-failure-param-alert-classname="error">
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<label for="password">Password</label>
<input name="password" id="password" type="password" autocomplete="off" required class="full-width" maxlength="128">
<hr />
<footer>
<button type="submit" class="">Update</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<div data-ui-modal class="modal width-large box close" data-button-hide="on" data-open-event="open-json">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
@ -134,6 +236,9 @@
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-name" class="link text-size-small">Update name</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-email" class="link text-size-small">Update Email</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-password" class="link text-size-small">Update Password</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-json" class="link text-size-small">View as JSON</button></li>
</ul>

View file

@ -294,6 +294,26 @@ services:
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: usage
container_name: appwrite-usage
restart: unless-stopped
networks:
- appwrite
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
appwrite-schedule:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>

View file

@ -1,34 +0,0 @@
<?php
use Appwrite\Extend\PDO;
use Utopia\App;
/** @var Utopia\Registry\Registry $register */
require_once __DIR__.'/init.php';
$register->set('db', function () {
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
$dbPass = App::getEnv('_APP_DB_PASS', '');
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
));
return $pdo;
});
$register->set('cache', function () { // Register cache connection
$redis = new Redis();
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});

View file

@ -4,7 +4,7 @@ use Appwrite\Resque\Worker;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Console::title('Audits V1 Worker');
Console::success(APP_NAME.' audits worker v1 has started');

View file

@ -9,7 +9,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Console::title('Certificates V1 Worker');
Console::success(APP_NAME.' certificates worker v1 has started');

View file

@ -5,7 +5,7 @@ use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Console::title('Database V1 Worker');
Console::success(APP_NAME.' database worker v1 has started'."\n");
@ -94,7 +94,7 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -120,7 +120,58 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
// The underlying database removes/rebuilds indexes when attribute is removed
// Update indexes table with changes
/** @var Document[] $indexes */
$indexes = $collection->getAttribute('indexes', []);
foreach ($indexes as $index) {
/** @var string[] $attributes */
$attributes = $index->getAttribute('attributes');
$lengths = $index->getAttribute('lengths');
$orders = $index->getAttribute('orders');
$found = \array_search($key, $attributes);
if ($found !== false) {
// If found, remove entry from attributes, lengths, and orders
// array_values wraps array_diff to reindex array keys
// when found attribute is removed from array
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
$lengths = \array_values(\array_diff($lengths, [$lengths[$found]]));
$orders = \array_values(\array_diff($orders, [$orders[$found]]));
if (empty($attributes)) {
$dbForInternal->deleteDocument('indexes', $index->getId());
} else {
$index
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN)
;
// Check if an index exists with the same attributes and orders
$exists = false;
foreach ($indexes as $existing) {
if ($existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
) {
$exists = true;
break;
}
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($collection, $index, $projectId);
} else {
$dbForInternal->updateDocument('indexes', $index->getId(), $index);
}
}
}
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -150,7 +201,7 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -168,7 +219,7 @@ class DatabaseV1 extends Worker
try {
if(!$dbForExternal->deleteIndex($collectionId, $key)) {
throw new Exception('Failed to delete Attribute');
throw new Exception('Failed to delete index');
}
$dbForInternal->deleteDocument('indexes', $index->getId());
@ -177,6 +228,6 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
}

View file

@ -11,15 +11,21 @@ use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\CLI\Console;
use Utopia\Audit\Audit;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Console::title('Deletes V1 Worker');
Console::success(APP_NAME.' deletes worker v1 has started'."\n");
class DeletesV1 extends Worker
{
/**
* @var array
*/
public $args = [];
/**
* @var Database
*/
protected $consoleDB = null;
public function init(): void
@ -33,11 +39,14 @@ class DeletesV1 extends Worker
switch (strval($type)) {
case DELETE_TYPE_DOCUMENT:
$document = $this->args['document'] ?? '';
$document = $this->args['document'] ?? [];
$document = new Document($document);
switch ($document->getCollection()) {
// TODO@kodumbeats define these as constants somewhere
case 'collections':
$this->deleteCollection($document, $projectId);
break;
case 'projects':
$this->deleteProject($document);
break;
@ -72,7 +81,10 @@ class DeletesV1 extends Worker
$document = new Document($this->args['document']);
$this->deleteCertificates($document);
break;
case DELETE_TYPE_USAGE:
$this->deleteUsageStats($this->args['timestamp1d'], $this->args['timestamp30m']);
break;
default:
Console::error('No delete operation for type: '.$type);
break;
@ -82,12 +94,58 @@ class DeletesV1 extends Worker
public function shutdown(): void
{
}
/**
* @param Document $document teams document
* @param string $projectId
*/
protected function deleteCollection(Document $document, string $projectId): void
{
$collectionId = $document->getId();
$dbForInternal = $this->getInternalDB($projectId);
$dbForExternal = $this->getExternalDB($projectId);
$this->deleteByGroup('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$collectionId])
], $dbForInternal);
$this->deleteByGroup('indexes', [
new Query('collectionId', Query::TYPE_EQUAL, [$collectionId])
], $dbForInternal);
$dbForExternal->deleteCollection($collectionId);
}
/**
* @param int $timestamp1d
* @param int $timestamp30m
*/
protected function deleteUsageStats(int $timestamp1d, int $timestamp30m) {
$this->deleteForProjectIds(function($projectId) use ($timestamp1d, $timestamp30m) {
if (!($dbForInternal = $this->getInternalDB($projectId))) {
throw new Exception('Failed to get projectDB for project '.$projectId);
}
// Delete Usage stats
$this->deleteByGroup('stats', [
new Query('time', Query::TYPE_LESSER, [$timestamp1d]),
new Query('period', Query::TYPE_EQUAL, ['1d']),
], $dbForInternal);
$this->deleteByGroup('stats', [
new Query('time', Query::TYPE_LESSER, [$timestamp30m]),
new Query('period', Query::TYPE_EQUAL, ['30m']),
], $dbForInternal);
});
}
/**
* @param Document $document teams document
* @param string $projectId
*/
protected function deleteMemberships(Document $document, $projectId) {
protected function deleteMemberships(Document $document, string $projectId): void
{
$teamId = $document->getAttribute('teamId', '');
// Delete Memberships
@ -99,7 +157,7 @@ class DeletesV1 extends Worker
/**
* @param Document $document project document
*/
protected function deleteProject(Document $document)
protected function deleteProject(Document $document): void
{
$projectId = $document->getId();
// Delete all DBs
@ -118,7 +176,7 @@ class DeletesV1 extends Worker
* @param Document $document user document
* @param string $projectId
*/
protected function deleteUser(Document $document, $projectId)
protected function deleteUser(Document $document, string $projectId): void
{
$userId = $document->getId();
@ -143,9 +201,9 @@ class DeletesV1 extends Worker
/**
* @param int $timestamp
*/
protected function deleteExecutionLogs($timestamp)
protected function deleteExecutionLogs(int $timestamp): void
{
$this->deleteForProjectIds(function($projectId) use ($timestamp) {
$this->deleteForProjectIds(function(string $projectId) use ($timestamp) {
if (!($dbForInternal = $this->getInternalDB($projectId))) {
throw new Exception('Failed to get projectDB for project '.$projectId);
}
@ -160,7 +218,7 @@ class DeletesV1 extends Worker
/**
* @param int $timestamp
*/
protected function deleteAbuseLogs($timestamp)
protected function deleteAbuseLogs(int $timestamp): void
{
if($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
@ -180,7 +238,7 @@ class DeletesV1 extends Worker
/**
* @param int $timestamp
*/
protected function deleteAuditLogs($timestamp)
protected function deleteAuditLogs(int $timestamp): void
{
if($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
@ -198,7 +256,7 @@ class DeletesV1 extends Worker
* @param Document $document function document
* @param string $projectId
*/
protected function deleteFunction(Document $document, $projectId)
protected function deleteFunction(Document $document, string $projectId): void
{
$dbForInternal = $this->getInternalDB($projectId);
$device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId);
@ -255,7 +313,7 @@ class DeletesV1 extends Worker
/**
* @param callable $callback
*/
protected function deleteForProjectIds(callable $callback)
protected function deleteForProjectIds(callable $callback): void
{
$count = 0;
$chunk = 0;
@ -266,12 +324,12 @@ class DeletesV1 extends Worker
$executionStart = \microtime(true);
while($sum === $limit) {
$chunk++;
Authorization::disable();
$projects = $this->getConsoleDB()->find('projects', [], $limit);
$projects = $this->getConsoleDB()->find('projects', [], $limit, ($chunk * $limit));
Authorization::reset();
$chunk++;
$projectIds = array_map (function ($project) {
return $project->getId();
}, $projects);
@ -295,7 +353,7 @@ class DeletesV1 extends Worker
* @param Database $database
* @param callable $callback
*/
protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null)
protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void
{
$count = 0;
$chunk = 0;
@ -331,9 +389,8 @@ class DeletesV1 extends Worker
/**
* @param Document $document certificates document
* @return Database
*/
protected function deleteCertificates(Document $document)
protected function deleteCertificates(Document $document): void
{
$domain = $document->getAttribute('domain');
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;

View file

@ -13,7 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Runtime::enableCoroutine(0);

View file

@ -6,7 +6,7 @@ use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Locale\Locale;
require_once __DIR__ . '/../workers.php';
require_once __DIR__ . '/../init.php';
Console::title('Mails V1 Worker');
Console::success(APP_NAME . ' mails worker v1 has started' . "\n");

View file

@ -4,7 +4,7 @@ use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
require_once __DIR__.'/../workers.php';
require_once __DIR__.'/../init.php';
Console::title('Webhooks V1 Worker');
Console::success(APP_NAME.' webhooks worker v1 has started');

3
bin/usage Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php usage $@

View file

@ -45,7 +45,7 @@
"utopia-php/cache": "0.4.*",
"utopia-php/cli": "0.11.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "dev-feat-adjusted-query-validator as 0.10.0",
"utopia-php/database": "0.10.0",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",
@ -63,12 +63,7 @@
"adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0"
},
"repositories": [
{
"type": "git",
"url": "https://github.com/utopia-php/database"
}
],
"repositories": [],
"require-dev": {
"appwrite/sdk-generator": "0.13.0",
"swoole/ide-helper": "4.6.7",

170
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "175f077512c575216c4c88f1d33c6d00",
"content-hash": "dfb8fa19daa736b3687617c98f309983",
"packages": [
{
"name": "adhocore/jwt",
@ -248,16 +248,16 @@
},
{
"name": "chillerlan/php-settings-container",
"version": "2.1.1",
"version": "2.1.2",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "98ccc1b31b31a53bcb563465c4961879b2b93096"
"reference": "ec834493a88682dd69652a1eeaf462789ed0c5f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/98ccc1b31b31a53bcb563465c4961879b2b93096",
"reference": "98ccc1b31b31a53bcb563465c4961879b2b93096",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/ec834493a88682dd69652a1eeaf462789ed0c5f5",
"reference": "ec834493a88682dd69652a1eeaf462789ed0c5f5",
"shasum": ""
},
"require": {
@ -307,7 +307,7 @@
"type": "ko_fi"
}
],
"time": "2021-01-06T15:57:03+00:00"
"time": "2021-09-06T15:17:01+00:00"
},
{
"name": "colinmollenhour/credis",
@ -355,16 +355,16 @@
},
{
"name": "composer/package-versions-deprecated",
"version": "1.11.99.2",
"version": "1.11.99.4",
"source": {
"type": "git",
"url": "https://github.com/composer/package-versions-deprecated.git",
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c"
"reference": "b174585d1fe49ceed21928a945138948cb394600"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c6522afe5540d5fc46675043d3ed5a45a740b27c",
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c",
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
"reference": "b174585d1fe49ceed21928a945138948cb394600",
"shasum": ""
},
"require": {
@ -408,7 +408,7 @@
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
"support": {
"issues": "https://github.com/composer/package-versions-deprecated/issues",
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.2"
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
},
"funding": [
{
@ -424,7 +424,7 @@
"type": "tidelift"
}
],
"time": "2021-05-24T07:46:03+00:00"
"time": "2021-09-13T08:41:34+00:00"
},
{
"name": "dragonmantank/cron-expression",
@ -1984,11 +1984,17 @@
},
{
"name": "utopia-php/database",
"version": "dev-feat-adjusted-query-validator",
"version": "0.10.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database",
"reference": "cb73391371f70ddb54bc0000064b15c5f173cb7a"
"url": "https://github.com/utopia-php/database.git",
"reference": "b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8",
"reference": "b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8",
"shasum": ""
},
"require": {
"ext-mongodb": "*",
@ -2011,11 +2017,7 @@
"Utopia\\Database\\": "src/Database"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Database"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -2037,7 +2039,11 @@
"upf",
"utopia"
],
"time": "2021-08-23T14:18:47+00:00"
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.10.0"
},
"time": "2021-10-04T17:23:25+00:00"
},
{
"name": "utopia-php/domains",
@ -2576,16 +2582,16 @@
"packages-dev": [
{
"name": "amphp/amp",
"version": "v2.6.0",
"version": "v2.6.1",
"source": {
"type": "git",
"url": "https://github.com/amphp/amp.git",
"reference": "caa95edeb1ca1bf7532e9118ede4a3c3126408cc"
"reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/amp/zipball/caa95edeb1ca1bf7532e9118ede4a3c3126408cc",
"reference": "caa95edeb1ca1bf7532e9118ede4a3c3126408cc",
"url": "https://api.github.com/repos/amphp/amp/zipball/c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
"reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
"shasum": ""
},
"require": {
@ -2653,7 +2659,7 @@
"support": {
"irc": "irc://irc.freenode.org/amphp",
"issues": "https://github.com/amphp/amp/issues",
"source": "https://github.com/amphp/amp/tree/v2.6.0"
"source": "https://github.com/amphp/amp/tree/v2.6.1"
},
"funding": [
{
@ -2661,7 +2667,7 @@
"type": "github"
}
],
"time": "2021-07-16T20:06:06+00:00"
"time": "2021-09-23T18:43:08+00:00"
},
{
"name": "amphp/byte-stream",
@ -3383,16 +3389,16 @@
},
{
"name": "nikic/php-parser",
"version": "v4.12.0",
"version": "v4.13.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "6608f01670c3cc5079e18c1dab1104e002579143"
"reference": "50953a2691a922aa1769461637869a0a2faa3f53"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143",
"reference": "6608f01670c3cc5079e18c1dab1104e002579143",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53",
"reference": "50953a2691a922aa1769461637869a0a2faa3f53",
"shasum": ""
},
"require": {
@ -3433,9 +3439,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0"
},
"time": "2021-07-21T10:44:31+00:00"
"time": "2021-09-20T12:20:58+00:00"
},
{
"name": "openlss/lib-array2xml",
@ -3712,16 +3718,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.4.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
"reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"shasum": ""
},
"require": {
@ -3729,7 +3735,8 @@
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*"
"ext-tokenizer": "*",
"psalm/phar": "^4.8"
},
"type": "library",
"extra": {
@ -3755,39 +3762,39 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1"
},
"time": "2020-09-17T18:55:26+00:00"
"time": "2021-10-02T14:08:47+00:00"
},
{
"name": "phpspec/prophecy",
"version": "1.13.0",
"version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
"reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
"reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
"reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2",
"php": "^7.2 || ~8.0, <8.1",
"php": "^7.2 || ~8.0, <8.2",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^6.0",
"phpspec/phpspec": "^6.0 || ^7.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11.x-dev"
"dev-master": "1.x-dev"
}
},
"autoload": {
@ -3822,29 +3829,29 @@
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/1.13.0"
"source": "https://github.com/phpspec/prophecy/tree/1.14.0"
},
"time": "2021-03-17T13:42:18+00:00"
"time": "2021-09-10T09:02:12+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.6",
"version": "9.2.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "f6293e1b30a2354e8428e004689671b83871edde"
"reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
"reference": "f6293e1b30a2354e8428e004689671b83871edde",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218",
"reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.10.2",
"nikic/php-parser": "^4.12.0",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -3893,7 +3900,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7"
},
"funding": [
{
@ -3901,7 +3908,7 @@
"type": "github"
}
],
"time": "2021-03-28T07:26:59+00:00"
"time": "2021-09-17T05:39:03+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -5313,16 +5320,16 @@
},
{
"name": "symfony/console",
"version": "v5.3.6",
"version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2"
"reference": "8b1008344647462ae6ec57559da166c2bfa5e16a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/51b71afd6d2dc8f5063199357b9880cea8d8bfe2",
"reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2",
"url": "https://api.github.com/repos/symfony/console/zipball/8b1008344647462ae6ec57559da166c2bfa5e16a",
"reference": "8b1008344647462ae6ec57559da166c2bfa5e16a",
"shasum": ""
},
"require": {
@ -5392,7 +5399,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.3.6"
"source": "https://github.com/symfony/console/tree/v5.3.7"
},
"funding": [
{
@ -5408,7 +5415,7 @@
"type": "tidelift"
}
],
"time": "2021-07-27T19:10:22+00:00"
"time": "2021-08-25T20:02:16+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -5882,16 +5889,16 @@
},
{
"name": "symfony/string",
"version": "v5.3.3",
"version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1"
"reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"url": "https://api.github.com/repos/symfony/string/zipball/8d224396e28d30f81969f083a58763b8b9ceb0a5",
"reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5",
"shasum": ""
},
"require": {
@ -5945,7 +5952,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.3.3"
"source": "https://github.com/symfony/string/tree/v5.3.7"
},
"funding": [
{
@ -5961,7 +5968,7 @@
"type": "tidelift"
}
],
"time": "2021-06-27T11:44:38+00:00"
"time": "2021-08-26T08:00:08+00:00"
},
{
"name": "theseer/tokenizer",
@ -6015,16 +6022,16 @@
},
{
"name": "twig/twig",
"version": "v2.14.6",
"version": "v2.14.7",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "27e5cf2b05e3744accf39d4c68a3235d9966d260"
"reference": "8e202327ee1ed863629de9b18a5ec70ac614d88f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/27e5cf2b05e3744accf39d4c68a3235d9966d260",
"reference": "27e5cf2b05e3744accf39d4c68a3235d9966d260",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/8e202327ee1ed863629de9b18a5ec70ac614d88f",
"reference": "8e202327ee1ed863629de9b18a5ec70ac614d88f",
"shasum": ""
},
"require": {
@ -6034,7 +6041,7 @@
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
},
"type": "library",
"extra": {
@ -6078,7 +6085,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v2.14.6"
"source": "https://github.com/twigphp/Twig/tree/v2.14.7"
},
"funding": [
{
@ -6090,7 +6097,7 @@
"type": "tidelift"
}
],
"time": "2021-05-16T12:12:47+00:00"
"time": "2021-09-17T08:39:54+00:00"
},
{
"name": "vimeo/psalm",
@ -6248,18 +6255,9 @@
"time": "2015-12-17T08:42:14+00:00"
}
],
"aliases": [
{
"package": "utopia-php/database",
"version": "dev-feat-adjusted-query-validator",
"alias": "0.10.0",
"alias_normalized": "0.10.0.0"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"utopia-php/database": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View file

@ -73,7 +73,6 @@ services:
- mariadb
- redis
# - clamav
- influxdb
entrypoint:
- php
- -e
@ -112,8 +111,6 @@ services:
- _APP_SMTP_USERNAME
- _APP_SMTP_PASSWORD
- _APP_USAGE_STATS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_STORAGE_LIMIT
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_CONTAINERS
@ -349,6 +346,37 @@ services:
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
entrypoint:
- php
- -e
- /usr/src/code/app/cli.php
- usage
container_name: appwrite-usage
build:
context: .
args:
- DEBUG=false
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./dev:/usr/local/dev
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_SYNC_INTERVAL
appwrite-schedule:
entrypoint: schedule
container_name: appwrite-schedule

View file

@ -487,8 +487,17 @@ if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),delete:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateEmail:(userId,email)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
let path='/users/{userId}/email'.replace('{userId}',userId);let payload={};if(typeof email!=='undefined'){payload['email']=email;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(userId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
let path='/users/{userId}/name'.replace('{userId}',userId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(userId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/{userId}/password'.replace('{userId}',userId);let payload={};if(typeof password!=='undefined'){payload['password']=password;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;}

View file

@ -487,8 +487,17 @@ if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),delete:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateEmail:(userId,email)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
let path='/users/{userId}/email'.replace('{userId}',userId);let payload={};if(typeof email!=='undefined'){payload['email']=email;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(userId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
let path='/users/{userId}/name'.replace('{userId}',userId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(userId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/{userId}/password'.replace('{userId}',userId);let payload={};if(typeof password!=='undefined'){payload['password']=password;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;}

View file

@ -4238,6 +4238,33 @@
'content-type': 'application/json',
}, payload);
}),
/**
* Update Email
*
* Update the user email by its unique ID.
*
* @param {string} userId
* @param {string} email
* @throws {AppwriteException}
* @returns {Promise}
*/
updateEmail: (userId, email) => __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
let path = '/users/{userId}/email'.replace('{userId}', userId);
let payload = {};
if (typeof email !== 'undefined') {
payload['email'] = email;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('patch', uri, {
'content-type': 'application/json',
}, payload);
}),
/**
* Get User Logs
*
@ -4258,6 +4285,60 @@
'content-type': 'application/json',
}, payload);
}),
/**
* Update Name
*
* Update the user name by its unique ID.
*
* @param {string} userId
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
updateName: (userId, name) => __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof name === 'undefined') {
throw new AppwriteException('Missing required parameter: "name"');
}
let path = '/users/{userId}/name'.replace('{userId}', userId);
let payload = {};
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('patch', uri, {
'content-type': 'application/json',
}, payload);
}),
/**
* Update Password
*
* Update the user password by its unique ID.
*
* @param {string} userId
* @param {string} password
* @throws {AppwriteException}
* @returns {Promise}
*/
updatePassword: (userId, password) => __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/{userId}/password'.replace('{userId}', userId);
let payload = {};
if (typeof password !== 'undefined') {
payload['password'] = password;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('patch', uri, {
'content-type': 'application/json',
}, payload);
}),
/**
* Get User Preferences
*

View file

@ -4,7 +4,6 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use stdClass;
use Utopia\Validator;
class OpenAPI3 extends Format
@ -21,6 +20,34 @@ class OpenAPI3 extends Format
return 'Open API 3';
}
/**
* Get Used Models
*
* Recursively get all used models
*
* @param object $model
* @param array $models
*
* @return void
*/
protected function getUsedModels($model, array &$usedModels)
{
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $model;
return;
}
if (!is_object($model)) return;
foreach ($model->getRules() as $rule) {
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$this->getUsedModels($type, $usedModels);
}
} else {
$this->getUsedModels($rule['type'], $usedModels);
}
}
}
/**
* Parse
*
@ -71,7 +98,7 @@ class OpenAPI3 extends Format
if (isset($output['components']['securitySchemes']['Project'])) {
$output['components']['securitySchemes']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
}
if (isset($output['components']['securitySchemes']['Key'])) {
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
}
@ -79,7 +106,7 @@ class OpenAPI3 extends Format
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
$output['components']['securitySchemes']['Locale']['x-appwrite'] = ['demo' => 'en'];
}
@ -103,7 +130,7 @@ class OpenAPI3 extends Format
$id = $route->getLabel('sdk.method', \uniqid());
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../../'.$route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
$model = $route->getLabel('sdk.response.model', 'none');
$routeSecurity = $route->getLabel('sdk.auth', []);
$sdkPlatofrms = [];
@ -127,7 +154,7 @@ class OpenAPI3 extends Format
if(empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default').ucfirst($id),
@ -153,13 +180,24 @@ class OpenAPI3 extends Format
];
foreach ($this->models as $key => $value) {
if($value->getType() === $model) {
$model = $value;
break;
if(\is_array($model)) {
$model = \array_map(function($m) use($value) {
if($m === $value->getType()) {
return $value;
}
return $m;
}, $model);
} else {
if($value->getType() === $model) {
$model = $value;
break;
}
}
}
if($model->isNone()) {
if(!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => (in_array($produces, [
'image/*',
@ -176,17 +214,43 @@ class OpenAPI3 extends Format
// ],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/components/schemas/'.$model->getType(),
if(\is_array($model)) {
$modelDescription = \join(', or ', \array_map(function ($m) {
return $m->getName();
}, $model));
// model has multiple possible responses, we will use oneOf
foreach ($model as $m) {
$usedModels[] = $m->getType();
}
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(function($m) {
return ['$ref' => '#/components/schemas/'.$m->getType()];
}, $model)
],
],
],
],
];
];
} else {
// Response definition using one type
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/components/schemas/'.$model->getType(),
],
],
],
];
}
}
if($route->getLabel('sdk.response.code', 500) === 204) {
@ -196,7 +260,7 @@ class OpenAPI3 extends Format
if ((!empty($scope))) { // && 'public' != $scope
$securities = ['Project' => []];
foreach($route->getLabel('sdk.auth', []) as $security) {
if(array_key_exists($security, $this->keys)) {
$securities[$security] = [];
@ -255,7 +319,7 @@ class OpenAPI3 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
$param['default'] = (empty($param['default'])) ? new stdClass() : $param['default'];
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = '{}';
//$node['schema']['format'] = 'json';
@ -352,11 +416,7 @@ class OpenAPI3 extends Format
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
}
foreach ($this->models as $model) {
foreach ($model->getRules() as $rule) {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
}
}
$this->getUsedModels($model, $usedModels);
}
foreach ($this->models as $model) {
if (!in_array($model->getType(), $usedModels) && $model->getType() !== 'error') {
@ -378,7 +438,7 @@ class OpenAPI3 extends Format
if($model->isAny()) {
$output['components']['schemas'][$model->getType()]['additionalProperties'] = true;
}
if(!empty($required)) {
$output['components']['schemas'][$model->getType()]['required'] = $required;
}
@ -393,7 +453,7 @@ class OpenAPI3 extends Format
case 'json':
$type = 'string';
break;
case 'integer':
$type = 'integer';
$format = 'int32';
@ -403,18 +463,39 @@ class OpenAPI3 extends Format
$type = 'number';
$format = 'float';
break;
case 'double':
$type = 'number';
$format = 'double';
break;
case 'boolean':
$type = 'boolean';
break;
default:
$type = 'object';
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
$items = [
'$ref' => '#/components/schemas/'.$rule['type'],
];
if(\is_array($rule['type'])) {
if($rule['array']) {
$items = [
'anyOf' => \array_map(function($type) {
return ['$ref' => '#/components/schemas/'.$type];
}, $rule['type'])
];
} else {
$items = [
'oneOf' => \array_map(function($type) {
return ['$ref' => '#/components/schemas/'.$type];
}, $rule['type'])
];
}
} else {
$items = [
'$ref' => '#/components/schemas/'.$rule['type'],
];
}
break;
}

View file

@ -4,7 +4,6 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use stdClass;
use Utopia\Validator;
class Swagger2 extends Format
@ -21,6 +20,34 @@ class Swagger2 extends Format
return 'Swagger 2';
}
/**
* Get Used Models
*
* Recursively get all used models
*
* @param object $model
* @param array $models
*
* @return void
*/
protected function getUsedModels($model, array &$usedModels)
{
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $model;
return;
}
if (!is_object($model)) return;
foreach ($model->getRules() as $rule) {
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$this->getUsedModels($type, $usedModels);
}
} else {
$this->getUsedModels($rule['type'], $usedModels);
}
}
}
/**
* Parse
*
@ -69,15 +96,15 @@ class Swagger2 extends Format
if (isset($output['securityDefinitions']['Project'])) {
$output['securityDefinitions']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
}
if (isset($output['securityDefinitions']['Key'])) {
$output['securityDefinitions']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
}
if (isset($output['securityDefinitions']['Locale'])) {
$output['securityDefinitions']['Locale']['x-appwrite'] = ['demo' => 'en'];
}
@ -125,7 +152,7 @@ class Swagger2 extends Format
if(empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default').ucfirst($id),
@ -155,13 +182,22 @@ class Swagger2 extends Format
}
foreach ($this->models as $key => $value) {
if($value->getType() === $model) {
$model = $value;
break;
if(\is_array($model)) {
$model = \array_map(function($m) use($value) {
if($m === $value->getType()) {
return $value;
}
return $m;
}, $model);
} else {
if($value->getType() === $model) {
$model = $value;
break;
}
}
}
if($model->isNone()) {
if(!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => (in_array($produces, [
'image/*',
@ -178,13 +214,41 @@ class Swagger2 extends Format
],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'schema' => [
'$ref' => '#/definitions/'.$model->getType(),
],
];
if(\is_array($model)) {
$modelDescription = \join(', or ', \array_map(function ($m) {
return $m->getName();
}, $model));
// model has multiple possible responses, we will use oneOf
foreach ($model as $m) {
$usedModels[] = $m->getType();
}
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(function($m) {
return ['$ref' => '#/definitions/'.$m->getType()];
}, $model)
],
],
],
];
} else {
// Response definition using one type
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/definitions/'.$model->getType(),
],
],
],
];
}
}
if(in_array($route->getLabel('sdk.response.code', 500), [204, 301, 302, 308], true)) {
@ -194,7 +258,7 @@ class Swagger2 extends Format
if ((!empty($scope))) { // && 'public' != $scope
$securities = ['Project' => []];
foreach($route->getLabel('sdk.auth', []) as $security) {
if(array_key_exists($security, $this->keys)) {
$securities[$security] = [];
@ -204,7 +268,7 @@ class Swagger2 extends Format
$temp['x-appwrite']['auth'] = array_slice($securities, 0, $this->authCount);
$temp['security'][] = $securities;
}
$body = [
'name' => 'payload',
'in' => 'body',
@ -252,7 +316,7 @@ class Swagger2 extends Format
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
$node['type'] = 'object';
$param['default'] = (empty($param['default'])) ? new stdClass() : $param['default'];
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = '{}';
//$node['format'] = 'json';
break;
@ -354,15 +418,9 @@ class Swagger2 extends Format
$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->getUsedModels($model, $usedModels);
}
foreach ($this->models as $model) {
if (!in_array($model->getType(), $usedModels)) {
continue;
@ -383,7 +441,7 @@ class Swagger2 extends Format
if($model->isAny()) {
$output['definitions'][$model->getType()]['additionalProperties'] = true;
}
if(!empty($required)) {
$output['definitions'][$model->getType()]['required'] = $required;
}
@ -398,7 +456,7 @@ class Swagger2 extends Format
case 'json':
$type = 'string';
break;
case 'integer':
$type = 'integer';
$format = 'int32';
@ -408,19 +466,40 @@ class Swagger2 extends Format
$type = 'number';
$format = 'float';
break;
case 'double':
$type = 'number';
$format = 'double';
break;
case 'boolean':
$type = 'boolean';
break;
default:
$type = 'object';
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
$items = [
'type' => $type,
'$ref' => '#/definitions/'.$rule['type'],
];
if(\is_array($rule['type'])) {
if($rule['array']) {
$items = [
'anyOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
} else {
$items = [
'oneOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
}
} else {
$items = [
'type' => $type,
'$ref' => '#/definitions/'.$rule['type'],
];
}
break;
}

View file

@ -94,7 +94,7 @@ class Stats
$functionExecutionTime = $this->params['functionExecutionTime'] ?? 0;
$functionStatus = $this->params['functionStatus'] ?? '';
$tags = ",project={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
$tags = ",projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
// the global namespace is prepended to every key (optional)
$this->statsd->setNamespace($this->namespace);
@ -112,7 +112,70 @@ class Stats
$this->statsd->count('network.outbound' . $tags, $networkResponseSize);
$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',
];
foreach ($dbMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$tags = ",projectId={$projectId},collectionId=" . ($this->params['collectionId'] ?? '');
$this->statsd->increment($metric . $tags);
}
}
$storageMertics = [
'storage.files.create',
'storage.files.read',
'storage.files.update',
'storage.files.delete',
];
foreach ($storageMertics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$tags = ",projectId={$projectId},bucketId=" . ($this->params['bucketId'] ?? '');
$this->statsd->increment($metric . $tags);
}
}
$usersMetrics = [
'users.create',
'users.read',
'users.update',
'users.delete',
];
foreach ($usersMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$tags = ",projectId={$projectId}";
$this->statsd->increment($metric . $tags);
}
}
$sessionsMetrics = [
'users.sessions.create',
'users.sessions.delete',
];
foreach ($sessionsMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$tags = ",projectId={$projectId},provider=". ($this->params['provider'] ?? '');
$this->statsd->count($metric . $tags, $value);
}
}
if ($storage >= 1) {
$tags = ",projectId={$projectId},bucketId=" . ($this->params['bucketId'] ?? '');
$this->statsd->count('storage.all' . $tags, $storage);
}

View file

@ -11,6 +11,14 @@ use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeInteger;
use Appwrite\Utopia\Response\Model\AttributeFloat;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeEmail;
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\Continent;
@ -33,6 +41,7 @@ use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\Locale;
use Appwrite\Utopia\Response\Model\Log;
use Appwrite\Utopia\Response\Model\Membership;
use Appwrite\Utopia\Response\Model\Metric;
use Appwrite\Utopia\Response\Model\Permissions;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
@ -43,7 +52,13 @@ use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Mock; // Keep last
use stdClass;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
/**
* @method Response public function setStatusCode(int $code = 200)
@ -56,19 +71,37 @@ class Response extends SwooleResponse
const MODEL_LOG = 'log';
const MODEL_LOG_LIST = 'logList';
const MODEL_ERROR = 'error';
const MODEL_METRIC = 'metric';
const MODEL_METRIC_LIST = 'metricList';
const MODEL_ERROR_DEV = 'errorDev';
const MODEL_BASE_LIST = 'baseList';
const MODEL_USAGE_DATABASE = 'usageDatabase';
const MODEL_USAGE_COLLECTION = 'usageCollection';
const MODEL_USAGE_USERS = 'usageUsers';
const MODEL_USAGE_BUCKETS = 'usageBuckets';
const MODEL_USAGE_STORAGE = 'usageStorage';
const MODEL_USAGE_FUNCTIONS = 'usageFunctions';
const MODEL_USAGE_PROJECT = 'usageProject';
// Database
const MODEL_COLLECTION = 'collection';
const MODEL_COLLECTION_LIST = 'collectionList';
const MODEL_ATTRIBUTE = 'attribute';
const MODEL_ATTRIBUTE_LIST = 'attributeList';
const MODEL_INDEX = 'index';
const MODEL_INDEX_LIST = 'indexList';
const MODEL_DOCUMENT = 'document';
const MODEL_DOCUMENT_LIST = 'documentList';
// Database Attributes
const MODEL_ATTRIBUTE = 'attribute';
const MODEL_ATTRIBUTE_LIST = 'attributeList';
const MODEL_ATTRIBUTE_STRING = 'attributeString';
const MODEL_ATTRIBUTE_INTEGER= 'attributeInteger';
const MODEL_ATTRIBUTE_FLOAT= 'attributeFloat';
const MODEL_ATTRIBUTE_BOOLEAN= 'attributeBoolean';
const MODEL_ATTRIBUTE_EMAIL= 'attributeEmail';
const MODEL_ATTRIBUTE_IP= 'attributeIp';
const MODEL_ATTRIBUTE_URL= 'attributeUrl';
// Users
const MODEL_USER = 'user';
const MODEL_USER_LIST = 'userList';
@ -125,6 +158,7 @@ class Response extends SwooleResponse
// Deprecated
const MODEL_PERMISSIONS = 'permissions';
const MODEL_RULE = 'rule';
const MODEL_TASK = 'task';
// Tests (keep last)
const MODEL_MOCK = 'mock';
@ -154,7 +188,6 @@ class Response extends SwooleResponse
->setModel(new ErrorDev())
// Lists
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
->setModel(new BaseList('Attributes List', self::MODEL_ATTRIBUTE_LIST, 'attributes', self::MODEL_ATTRIBUTE))
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
@ -176,9 +209,18 @@ class Response extends SwooleResponse
->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE))
->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY))
->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 Collection())
->setModel(new Attribute())
->setModel(new AttributeList())
->setModel(new AttributeString())
->setModel(new AttributeInteger())
->setModel(new AttributeFloat())
->setModel(new AttributeBoolean())
->setModel(new AttributeEmail())
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())
@ -204,6 +246,14 @@ class Response extends SwooleResponse
->setModel(new Language())
->setModel(new Currency())
->setModel(new Phone())
->setModel(new Metric())
->setModel(new UsageDatabase())
->setModel(new UsageCollection())
->setModel(new UsageUsers())
->setModel(new UsageStorage())
->setModel(new UsageBuckets())
->setModel(new UsageFunctions())
->setModel(new UsageProject())
// Verification
// Recovery
// Tests (keep last)
@ -277,7 +327,7 @@ class Response extends SwooleResponse
$output = self::getFilter()->parse($output, $model);
}
$this->json(!empty($output) ? $output : new stdClass());
$this->json(!empty($output) ? $output : new \stdClass());
}
/**
@ -302,7 +352,7 @@ class Response extends SwooleResponse
$document = $model->filter($document);
foreach ($model->getRules() as $key => $rule) {
if (!$document->isSet($key)) {
if (!$document->isSet($key) && $rule['require']) { // do not set attribute in response if not required
if (!is_null($rule['default'])) {
$document->setAttribute($key, $rule['default']);
} else {
@ -317,15 +367,33 @@ class Response extends SwooleResponse
foreach ($data[$key] as &$item) {
if ($item instanceof Document) {
if (!array_key_exists($rule['type'], $this->models)) {
throw new Exception('Missing model for rule: '. $rule['type']);
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$condition = false;
foreach ($this->getModel($type)->conditions as $attribute => $val) {
$condition = $item->getAttribute($attribute) === $val;
if(!$condition) {
break;
}
}
if ($condition) {
$ruleType = $type;
break;
}
}
} else {
$ruleType = $rule['type'];
}
$item = $this->output($item, $rule['type']);
if (!array_key_exists($ruleType, $this->models)) {
throw new Exception('Missing model for rule: '. $ruleType);
}
$item = $this->output($item, $ruleType);
}
}
}
$output[$key] = $data[$key];
}

View file

@ -8,7 +8,7 @@ abstract class Model
{
const TYPE_STRING = 'string';
const TYPE_INTEGER = 'integer';
const TYPE_FLOAT = 'float';
const TYPE_FLOAT = 'double';
const TYPE_BOOLEAN = 'boolean';
const TYPE_JSON = 'json';
@ -35,7 +35,7 @@ abstract class Model
/**
* Filter Document Structure
*
* @return string
* @return Document
*/
public function filter(Document $document): Document
{
@ -68,6 +68,10 @@ abstract class Model
/**
* Add a New Rule
* If rule is an array of documents with varying models
*
* @param string $key
* @param array $options
*/
protected function addRule(string $key, array $options): self
{
@ -77,7 +81,7 @@ abstract class Model
'description' => '',
'default' => null,
'example' => '',
'array' => false,
'array' => false
], $options);
return $this;

View file

@ -28,12 +28,6 @@ class Attribute extends Model
'default' => '',
'example' => 'available',
])
->addRule('size', [
'type' => self::TYPE_STRING,
'description' => 'Attribute size.',
'default' => 0,
'example' => 128,
])
->addRule('required', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is attribute required?',
@ -45,11 +39,13 @@ class Attribute extends Model
'description' => 'Is attribute an array?',
'default' => false,
'example' => false,
'required' => false
'require' => false
])
;
}
public array $conditions = [];
/**
* Get Name
*
@ -69,4 +65,4 @@ class Attribute extends Model
{
return Response::MODEL_ATTRIBUTE;
}
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeBoolean extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('default', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => false,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_BOOLEAN
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeBoolean';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_BOOLEAN;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEmail extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_EMAIL,
'example' => APP_DATABASE_ATTRIBUTE_EMAIL,
'array' => false,
'require' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'default@example.com',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_EMAIL
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeEmail';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_EMAIL;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeFloat extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('min', [
'type' => self::TYPE_FLOAT,
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'example' => 1.5,
'array' => false,
'require' => false,
])
->addRule('max', [
'type' => self::TYPE_FLOAT,
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'example' => 10.5,
'array' => false,
'require' => false,
])
->addRule('default', [
'type' => self::TYPE_FLOAT,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 2.5,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_FLOAT,
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeFloat';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_FLOAT;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeIP extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_IP,
'example' => APP_DATABASE_ATTRIBUTE_IP,
'array' => false,
'require' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => '192.0.2.0',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_IP
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeIP';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_IP;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeInteger extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('min', [
'type' => self::TYPE_INTEGER,
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'example' => 1,
'array' => false,
'require' => false,
])
->addRule('max', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'example' => 10,
'array' => false,
'require' => false,
])
->addRule('default', [
'type' => self::TYPE_INTEGER,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 10,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_INTEGER,
];
/**
* Get Name *
* @return string
*/
public function getName():string
{
return 'AttributeInteger';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_INTEGER;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class AttributeList extends Model
{
public function __construct()
{
$this
->addRule('sum', [
'type' => self::TYPE_INTEGER,
'description' => 'Total sum of items in the list.',
'default' => 0,
'example' => 5,
])
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'List of attributes.',
'default' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'Attributes List';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_LIST;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeString extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('size', [
'type' => self::TYPE_STRING,
'description' => 'Attribute size.',
'default' => 0,
'example' => 128,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'default',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeString';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_STRING;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeURL extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_URL,
'example' => APP_DATABASE_ATTRIBUTE_URL,
'array' => false,
'required' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'http://example.com',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_URL
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeURL';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_URL;
}
}

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class Collection extends Model
{
@ -44,17 +43,25 @@ class Collection extends Model
'example' => 'document',
])
->addRule('attributes', [
'type' => Response::MODEL_ATTRIBUTE,
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'Collection attributes.',
'default' => [],
'example' => new stdClass,
'array' => true
'example' => new \stdClass,
'array' => true,
])
->addRule('indexes', [
'type' => Response::MODEL_INDEX,
'description' => 'Collection indexes.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Metric extends Model
{
public function __construct()
{
$this
->addRule('value', [
'type' => self::TYPE_INTEGER,
'description' => 'The value of this metric at the timestamp.',
'default' => -1,
'example' => 1,
])
->addRule('timestamp', [
'type' => self::TYPE_INTEGER,
'description' => 'The UNIX timestamp at which this metric was aggregated.',
'default' => 0,
'example' => 1592981250
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'Metric';
}
/**
* Get Collection
*
* @return string
*/
public function getType():string
{
return Response::MODEL_METRIC;
}
}

View file

@ -2,7 +2,6 @@
namespace Appwrite\Utopia\Response\Model;
use stdClass;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Config\Config;
@ -100,28 +99,28 @@ class Project extends Model
'type' => Response::MODEL_PLATFORM,
'description' => 'List of Platforms.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('webhooks', [
'type' => Response::MODEL_WEBHOOK,
'description' => 'List of Webhooks.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('keys', [
'type' => Response::MODEL_KEY,
'description' => 'List of API Keys.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('domains', [
'type' => Response::MODEL_DOMAIN,
'description' => 'List of Domains.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
;

View file

@ -0,0 +1,76 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageBuckets extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('files.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of files in this bucket.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('files.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('files.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('files.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('files.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'UsageBuckets';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_BUCKETS;
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageCollection extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('documents.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'UsageCollection';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_COLLECTION;
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageDatabase extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('documents.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of collections.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.delete', [
'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 'UsageDatabase';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_DATABASE;
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageFunctions extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('functions.executions', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('functions.failures', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution failures.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('functions.compute', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution duration.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'UsageFunctions';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_FUNCTIONS;
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageProject extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('requests', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of requests.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('network', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for consumed bandwidth.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('functions', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of documents.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of collections.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of users.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('storage', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'UsageProject';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_PROJECT;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageStorage extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('storage', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('files', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of files.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'StorageUsage';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_STORAGE;
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageUsers extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('users.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of users.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.provider.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created for a provider ( email, anonymous or oauth2 ).',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'UsageUsers';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_USAGE_USERS;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Tests\E2E\Scopes;
trait SideConsole
{
public function getHeaders():array
{
return [
'origin' => 'http://localhost',
'cookie' => 'a_session_console='. $this->getRoot()['session'],
'x-appwrite-mode' => 'admin'
];
}
/**
* @return string
*/
public function getSide()
{
return 'console';
}
}

View file

@ -342,7 +342,7 @@ class AccountCustomClientTest extends Scope
'password' => $password,
]);
$this->assertEquals($response['headers']['status-code'], 400);
$this->assertEquals($response['headers']['status-code'], 409);
/**
* Test for SUCCESS

View file

@ -73,7 +73,6 @@ trait DatabaseBase
$this->assertEquals($releaseYear['headers']['status-code'], 201);
$this->assertEquals($releaseYear['body']['key'], 'releaseYear');
$this->assertEquals($releaseYear['body']['type'], 'integer');
$this->assertEquals($releaseYear['body']['size'], 0);
$this->assertEquals($releaseYear['body']['required'], true);
$this->assertEquals($actors['headers']['status-code'], 201);
@ -101,6 +100,420 @@ trait DatabaseBase
return $data;
}
/**
* @depends testCreateAttributes
*/
public function testAttributeResponseModels(array $data): array
{
$collection= $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'name' => 'Response Models',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'Response Models');
$collectionId = $collection['body']['$id'];
$string = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'string',
'size' => 16,
'required' => false,
'default' => 'default',
]);
$email = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/email', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'email',
'required' => false,
'default' => 'default@example.com',
]);
$ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/ip', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'ip',
'required' => false,
'default' => '192.0.2.0',
]);
$url = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/url', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'url',
'required' => false,
'default' => 'http://example.com',
]);
$integer = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'integer',
'required' => false,
'min' => 1,
'max' => 5,
'default' => 3
]);
$float = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'float',
'required' => false,
'min' => 1.5,
'max' => 5.5,
'default' => 3.5
]);
$boolean = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/boolean', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'boolean',
'required' => false,
'default' => true,
]);
$this->assertEquals(201, $string['headers']['status-code']);
$this->assertEquals('string', $string['body']['key']);
$this->assertEquals('string', $string['body']['type']);
$this->assertEquals('processing', $string['body']['status']);
$this->assertEquals(false, $string['body']['required']);
$this->assertEquals(false, $string['body']['array']);
$this->assertEquals(16, $string['body']['size']);
$this->assertEquals('default', $string['body']['default']);
$this->assertEquals(201, $email['headers']['status-code']);
$this->assertEquals('email', $email['body']['key']);
$this->assertEquals('string', $email['body']['type']);
$this->assertEquals('processing', $email['body']['status']);
$this->assertEquals(false, $email['body']['required']);
$this->assertEquals(false, $email['body']['array']);
$this->assertEquals('email', $email['body']['format']);
$this->assertEquals('default@example.com', $email['body']['default']);
$this->assertEquals(201, $ip['headers']['status-code']);
$this->assertEquals('ip', $ip['body']['key']);
$this->assertEquals('string', $ip['body']['type']);
$this->assertEquals('processing', $ip['body']['status']);
$this->assertEquals(false, $ip['body']['required']);
$this->assertEquals(false, $ip['body']['array']);
$this->assertEquals('ip', $ip['body']['format']);
$this->assertEquals('192.0.2.0', $ip['body']['default']);
$this->assertEquals(201, $url['headers']['status-code']);
$this->assertEquals('url', $url['body']['key']);
$this->assertEquals('string', $url['body']['type']);
$this->assertEquals('processing', $url['body']['status']);
$this->assertEquals(false, $url['body']['required']);
$this->assertEquals(false, $url['body']['array']);
$this->assertEquals('url', $url['body']['format']);
$this->assertEquals('http://example.com', $url['body']['default']);
$this->assertEquals(201, $integer['headers']['status-code']);
$this->assertEquals('integer', $integer['body']['key']);
$this->assertEquals('integer', $integer['body']['type']);
$this->assertEquals('processing', $integer['body']['status']);
$this->assertEquals(false, $integer['body']['required']);
$this->assertEquals(false, $integer['body']['array']);
$this->assertEquals(1, $integer['body']['min']);
$this->assertEquals(5, $integer['body']['max']);
$this->assertEquals(3, $integer['body']['default']);
$this->assertEquals(201, $float['headers']['status-code']);
$this->assertEquals('float', $float['body']['key']);
$this->assertEquals('double', $float['body']['type']);
$this->assertEquals('processing', $float['body']['status']);
$this->assertEquals(false, $float['body']['required']);
$this->assertEquals(false, $float['body']['array']);
$this->assertEquals(1.5, $float['body']['min']);
$this->assertEquals(5.5, $float['body']['max']);
$this->assertEquals(3.5, $float['body']['default']);
$this->assertEquals(201, $boolean['headers']['status-code']);
$this->assertEquals('boolean', $boolean['body']['key']);
$this->assertEquals('boolean', $boolean['body']['type']);
$this->assertEquals('processing', $boolean['body']['status']);
$this->assertEquals(false, $boolean['body']['required']);
$this->assertEquals(false, $boolean['body']['array']);
$this->assertEquals(true, $boolean['body']['default']);
// wait for database worker to create attributes
sleep(5);
$stringResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$string['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$emailResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$email['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$ipResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$ip['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$urlResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$url['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$integerResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$integer['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$floatResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$float['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$booleanResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$boolean['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $stringResponse['headers']['status-code']);
$this->assertEquals($string['body']['key'], $stringResponse['body']['key']);
$this->assertEquals($string['body']['type'], $stringResponse['body']['type']);
$this->assertEquals('available', $stringResponse['body']['status']);
$this->assertEquals($string['body']['required'], $stringResponse['body']['required']);
$this->assertEquals($string['body']['array'], $stringResponse['body']['array']);
$this->assertEquals(16, $stringResponse['body']['size']);
$this->assertEquals($string['body']['default'], $stringResponse['body']['default']);
$this->assertEquals(200, $emailResponse['headers']['status-code']);
$this->assertEquals($email['body']['key'], $emailResponse['body']['key']);
$this->assertEquals($email['body']['type'], $emailResponse['body']['type']);
$this->assertEquals('available', $emailResponse['body']['status']);
$this->assertEquals($email['body']['required'], $emailResponse['body']['required']);
$this->assertEquals($email['body']['array'], $emailResponse['body']['array']);
$this->assertEquals($email['body']['format'], $emailResponse['body']['format']);
$this->assertEquals($email['body']['default'], $emailResponse['body']['default']);
$this->assertEquals(200, $ipResponse['headers']['status-code']);
$this->assertEquals($ip['body']['key'], $ipResponse['body']['key']);
$this->assertEquals($ip['body']['type'], $ipResponse['body']['type']);
$this->assertEquals('available', $ipResponse['body']['status']);
$this->assertEquals($ip['body']['required'], $ipResponse['body']['required']);
$this->assertEquals($ip['body']['array'], $ipResponse['body']['array']);
$this->assertEquals($ip['body']['format'], $ipResponse['body']['format']);
$this->assertEquals($ip['body']['default'], $ipResponse['body']['default']);
$this->assertEquals(200, $urlResponse['headers']['status-code']);
$this->assertEquals($url['body']['key'], $urlResponse['body']['key']);
$this->assertEquals($url['body']['type'], $urlResponse['body']['type']);
$this->assertEquals('available', $urlResponse['body']['status']);
$this->assertEquals($url['body']['required'], $urlResponse['body']['required']);
$this->assertEquals($url['body']['array'], $urlResponse['body']['array']);
$this->assertEquals($url['body']['format'], $urlResponse['body']['format']);
$this->assertEquals($url['body']['default'], $urlResponse['body']['default']);
$this->assertEquals(200, $integerResponse['headers']['status-code']);
$this->assertEquals($integer['body']['key'], $integerResponse['body']['key']);
$this->assertEquals($integer['body']['type'], $integerResponse['body']['type']);
$this->assertEquals('available', $integerResponse['body']['status']);
$this->assertEquals($integer['body']['required'], $integerResponse['body']['required']);
$this->assertEquals($integer['body']['array'], $integerResponse['body']['array']);
$this->assertEquals($integer['body']['min'], $integerResponse['body']['min']);
$this->assertEquals($integer['body']['max'], $integerResponse['body']['max']);
$this->assertEquals($integer['body']['default'], $integerResponse['body']['default']);
$this->assertEquals(200, $floatResponse['headers']['status-code']);
$this->assertEquals($float['body']['key'], $floatResponse['body']['key']);
$this->assertEquals($float['body']['type'], $floatResponse['body']['type']);
$this->assertEquals('available', $floatResponse['body']['status']);
$this->assertEquals($float['body']['required'], $floatResponse['body']['required']);
$this->assertEquals($float['body']['array'], $floatResponse['body']['array']);
$this->assertEquals($float['body']['min'], $floatResponse['body']['min']);
$this->assertEquals($float['body']['max'], $floatResponse['body']['max']);
$this->assertEquals($float['body']['default'], $floatResponse['body']['default']);
$this->assertEquals(200, $booleanResponse['headers']['status-code']);
$this->assertEquals($boolean['body']['key'], $booleanResponse['body']['key']);
$this->assertEquals($boolean['body']['type'], $booleanResponse['body']['type']);
$this->assertEquals('available', $booleanResponse['body']['status']);
$this->assertEquals($boolean['body']['required'], $booleanResponse['body']['required']);
$this->assertEquals($boolean['body']['array'], $booleanResponse['body']['array']);
$this->assertEquals($boolean['body']['default'], $booleanResponse['body']['default']);
$attributes = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/attributes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $attributes['headers']['status-code']);
$this->assertEquals(7, $attributes['body']['sum']);
$attributes = $attributes['body']['attributes'];
$this->assertIsArray($attributes);
$this->assertCount(7, $attributes);
$this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
$this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
$this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
$this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
$this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
$this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
$this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);
$this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
$this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
$this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
$this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
$this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
$this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
$this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);
$this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']);
$this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']);
$this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']);
$this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']);
$this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']);
$this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']);
$this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']);
$this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']);
$this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']);
$this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']);
$this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']);
$this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']);
$this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']);
$this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']);
$this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']);
$this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']);
$this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']);
$this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']);
$this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']);
$this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']);
$this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']);
$this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']);
$this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']);
$this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']);
$this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']);
$this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']);
$this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']);
$this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']);
$this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']);
$this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']);
$this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']);
$this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']);
$this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']);
$this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']);
$this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']);
$this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$attributes = $collection['body']['attributes'];
$this->assertIsArray($attributes);
$this->assertCount(7, $attributes);
$this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
$this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
$this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
$this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
$this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
$this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
$this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);
$this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
$this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
$this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
$this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
$this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
$this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
$this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);
$this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']);
$this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']);
$this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']);
$this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']);
$this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']);
$this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']);
$this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']);
$this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']);
$this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']);
$this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']);
$this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']);
$this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']);
$this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']);
$this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']);
$this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']);
$this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']);
$this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']);
$this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']);
$this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']);
$this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']);
$this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']);
$this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']);
$this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']);
$this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']);
$this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']);
$this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']);
$this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']);
$this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']);
$this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']);
$this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']);
$this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']);
$this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']);
$this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']);
$this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']);
$this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']);
$this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']);
return $data;
}
/**
* @depends testCreateAttributes
*/
@ -754,7 +1167,7 @@ trait DatabaseBase
// $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
// wait for worker to add attributes
sleep(2);
sleep(3);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
@ -1089,4 +1502,224 @@ trait DatabaseBase
return $data;
}
public function testEnforceCollectionPermissions()
{
$user = 'user:' . $this->getUser()['$id'];
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => [$user]
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions');
$this->assertEquals($collection['body']['permission'], 'collection');
$collectionId = $collection['body']['$id'];
sleep(2);
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute',
'size' => 64,
'required' => true,
]);
$this->assertEquals(201, $attribute['headers']['status-code'], 201);
$this->assertEquals('attribute', $attribute['body']['key']);
// wait for db to add attribute
sleep(2);
$index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'key_attribute',
'type' => 'key',
'attributes' => [$attribute['body']['key']],
]);
$this->assertEquals(201, $index['headers']['status-code']);
$this->assertEquals('key_attribute', $index['body']['key']);
// wait for db to add attribute
sleep(2);
$document1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'one',
],
'read' => [$user],
'write' => [$user],
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['sum']);
$this->assertCount(1, $documents['body']['documents']);
/*
* Test for Failure
*/
// Remove write permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$badDocument = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'bad',
],
'read' => [$user],
'write' => [$user],
]);
if($this->getSide() == 'client') {
$this->assertEquals(401, $badDocument['headers']['status-code']);
}
if($this->getSide() == 'server') {
$this->assertEquals(201, $badDocument['headers']['status-code']);
}
// Remove read permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(404, $documents['headers']['status-code']);
}
/**
* @depends testDefaultPermissions
*/
public function testUniqueIndexDuplicate(array $data): array
{
$uniqueIndex = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'unique_title',
'type' => 'unique',
'attributes' => ['title'],
]);
$this->assertEquals($uniqueIndex['headers']['status-code'], 201);
sleep(2);
// test for failure
$duplicate = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'title' => 'Captain America',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
]
],
'read' => ['user:'.$this->getUser()['$id']],
'write' => ['user:'.$this->getUser()['$id']],
]);
$this->assertEquals(409, $duplicate['headers']['status-code']);
// Test for exception when updating document to conflict
$document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'title' => 'Captain America 5',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
]
],
'read' => ['user:'.$this->getUser()['$id']],
'write' => ['user:'.$this->getUser()['$id']],
]);
$this->assertEquals(201, $document['headers']['status-code']);
// Test for exception when updating document to conflict
$duplicate = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['moviesId'] . '/documents/' . $document['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'title' => 'Captain America',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
]
],
'read' => ['user:'.$this->getUser()['$id']],
'write' => ['user:'.$this->getUser()['$id']],
]);
$this->assertEquals(409, $duplicate['headers']['status-code']);
return $data;
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Tests\E2E\Services\Database;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SideConsole;
class DatabaseConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
public function testCreateCollection():array
{
/**
* Test for SUCCESS
*/
$movies = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'collectionId' => 'unique()',
'name' => 'Movies',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals($movies['headers']['status-code'], 201);
$this->assertEquals($movies['body']['name'], 'Movies');
return ['moviesId' => $movies['body']['$id']];
}
public function testGetDatabaseUsage()
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/database/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/database/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 11);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['documents.count']);
$this->assertIsArray($response['body']['collections.count']);
$this->assertIsArray($response['body']['documents.create']);
$this->assertIsArray($response['body']['documents.read']);
$this->assertIsArray($response['body']['documents.update']);
$this->assertIsArray($response['body']['documents.delete']);
$this->assertIsArray($response['body']['collections.create']);
$this->assertIsArray($response['body']['collections.read']);
$this->assertIsArray($response['body']['collections.update']);
$this->assertIsArray($response['body']['collections.delete']);
}
/**
* @depends testCreateCollection
*/
public function testGetCollectionUsage(array $data)
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/database/'.$data['moviesId'].'/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/database/randomCollectionId/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 404);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/database/'.$data['moviesId'].'/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 6);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['documents.count']);
$this->assertIsArray($response['body']['documents.create']);
$this->assertIsArray($response['body']['documents.read']);
$this->assertIsArray($response['body']['documents.update']);
$this->assertIsArray($response['body']['documents.delete']);
}
}

View file

@ -241,6 +241,214 @@ class DatabaseCustomServerTest extends Scope
/**
* @depends testDeleteIndex
*/
public function testDeleteIndexOnDeleteAttribute($data)
{
$attribute1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute1',
'size' => 16,
'required' => true,
]);
$attribute2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute2',
'size' => 16,
'required' => true,
]);
$this->assertEquals(201, $attribute1['headers']['status-code']);
$this->assertEquals(201, $attribute2['headers']['status-code']);
$this->assertEquals('attribute1', $attribute1['body']['key']);
$this->assertEquals('attribute2', $attribute2['body']['key']);
sleep(2);
$index1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index1',
'type' => 'key',
'attributes' => ['attribute1', 'attribute2'],
'orders' => ['ASC', 'ASC'],
]);
$index2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index2',
'type' => 'key',
'attributes' => ['attribute2'],
]);
$this->assertEquals(201, $index1['headers']['status-code']);
$this->assertEquals(201, $index2['headers']['status-code']);
$this->assertEquals('index1', $index1['body']['key']);
$this->assertEquals('index2', $index2['body']['key']);
sleep(2);
// Expected behavior: deleting attribute2 will cause index2 to be dropped, and index1 rebuilt with a single key
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['collectionId'] . '/attributes/'. $attribute2['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
// wait for database worker to complete
sleep(2);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['collectionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['indexes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals($index1['body']['key'], $collection['body']['indexes'][0]['key']);
$this->assertIsArray($collection['body']['indexes'][0]['attributes']);
$this->assertCount(1, $collection['body']['indexes'][0]['attributes']);
$this->assertEquals($attribute1['body']['key'], $collection['body']['indexes'][0]['attributes'][0]);
// Delete attribute
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['collectionId'] . '/attributes/' . $attribute1['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
return $data;
}
public function testCleanupDuplicateIndexOnDeleteAttribute()
{
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'name' => 'TestCleanupDuplicateIndexOnDeleteAttribute',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertNotEmpty($collection['body']['$id']);
$collectionId = $collection['body']['$id'];
$attribute1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute1',
'size' => 16,
'required' => true,
]);
$attribute2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute2',
'size' => 16,
'required' => true,
]);
$this->assertEquals(201, $attribute1['headers']['status-code']);
$this->assertEquals(201, $attribute2['headers']['status-code']);
$this->assertEquals('attribute1', $attribute1['body']['key']);
$this->assertEquals('attribute2', $attribute2['body']['key']);
sleep(2);
$index1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index1',
'type' => 'key',
'attributes' => ['attribute1', 'attribute2'],
'orders' => ['ASC', 'ASC'],
]);
$index2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index2',
'type' => 'key',
'attributes' => ['attribute2'],
]);
$this->assertEquals(201, $index1['headers']['status-code']);
$this->assertEquals(201, $index2['headers']['status-code']);
$this->assertEquals('index1', $index1['body']['key']);
$this->assertEquals('index2', $index2['body']['key']);
sleep(2);
// Expected behavior: deleting attribute1 would cause index1 to be a duplicate of index2 and automatically removed
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $collectionId . '/attributes/'. $attribute1['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
// wait for database worker to complete
sleep(2);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['indexes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals($index2['body']['key'], $collection['body']['indexes'][0]['key']);
$this->assertIsArray($collection['body']['indexes'][0]['attributes']);
$this->assertCount(1, $collection['body']['indexes'][0]['attributes']);
$this->assertEquals($attribute2['body']['key'], $collection['body']['indexes'][0]['attributes'][0]);
// Delete attribute
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $collectionId . '/attributes/' . $attribute2['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
}
/**
* @depends testDeleteIndexOnDeleteAttribute
*/
public function testDeleteCollection($data)
{
$collectionId = $data['collectionId'];
@ -307,6 +515,101 @@ class DatabaseCustomServerTest extends Scope
$this->assertEquals($response['headers']['status-code'], 404);
}
public function testAttributeCountLimit()
{
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'name' => 'attributeCountLimit',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$collectionId = $collection['body']['$id'];
// load the collection up to the limit
for ($i=0; $i < 1012; $i++) {
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "attribute{$i}",
'required' => false,
]);
$this->assertEquals(201, $attribute['headers']['status-code']);
}
sleep(5);
$tooMany = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "tooMany",
'required' => false,
]);
$this->assertEquals(400, $tooMany['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooMany['body']['message']);
}
public function testAttributeRowWidthLimit()
{
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'attributeRowWidthLimit',
'name' => 'attributeRowWidthLimit',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'attributeRowWidthLimit');
$collectionId = $collection['body']['$id'];
// Add wide string attributes to approach row width limit
for ($i=0; $i < 15; $i++) {
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "attribute{$i}",
'size' => 1024,
'required' => true,
]);
$this->assertEquals($attribute['headers']['status-code'], 201);
}
sleep(5);
$tooWide = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'tooWide',
'size' => 1024,
'required' => true,
]);
$this->assertEquals(400, $tooWide['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooWide['body']['message']);
}
public function testIndexLimitException()
{
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
@ -403,5 +706,13 @@ class DatabaseCustomServerTest extends Scope
$this->assertEquals(400, $tooMany['headers']['status-code']);
$this->assertEquals('Index limit exceeded', $tooMany['body']['message']);
$collection = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $collection['headers']['status-code']);
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Tests\E2E\Services\Functions;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SideConsole;
class FunctionsConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
public function testCreateFunction():array
{
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'name' => 'Test',
'execute' => ['user:'.$this->getUser()['$id']],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
'funcKey3' => 'funcValue3',
],
'events' => [
'account.create',
'account.delete',
],
'schedule' => '0 0 1 1 *',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
return [
'functionId' => $function['body']['$id']
];
}
/**
* @depends testCreateFunction
*/
public function testGetCollectionUsage(array $data)
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '232h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/functions/randomFunctionId/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 4);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['functions.executions']);
$this->assertIsArray($response['body']['functions.failures']);
$this->assertIsArray($response['body']['functions.compute']);
}
}

View file

@ -215,24 +215,16 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(count($response['body']), 8);
$this->assertNotEmpty($response['body']);
$this->assertArrayHasKey('collections', $response['body']);
$this->assertArrayHasKey('documents', $response['body']);
$this->assertArrayHasKey('network', $response['body']);
$this->assertArrayHasKey('requests', $response['body']);
$this->assertArrayHasKey('storage', $response['body']);
$this->assertArrayHasKey('users', $response['body']);
$this->assertIsArray($response['body']['collections']['data']);
$this->assertIsInt($response['body']['collections']['total']);
$this->assertIsArray($response['body']['documents']['data']);
$this->assertIsInt($response['body']['documents']['total']);
$this->assertIsArray($response['body']['network']['data']);
$this->assertIsInt($response['body']['network']['total']);
$this->assertIsArray($response['body']['requests']['data']);
$this->assertIsInt($response['body']['requests']['total']);
$this->assertIsInt($response['body']['storage']['total']);
$this->assertIsArray($response['body']['users']['data']);
$this->assertIsInt($response['body']['users']['total']);
$this->assertEquals('30d', $response['body']['range']);
$this->assertIsArray($response['body']['requests']);
$this->assertIsArray($response['body']['network']);
$this->assertIsArray($response['body']['functions']);
$this->assertIsArray($response['body']['documents']);
$this->assertIsArray($response['body']['collections']);
$this->assertIsArray($response['body']['users']);
$this->assertIsArray($response['body']['storage']);
/**
* Test for FAILURE

View file

@ -2,13 +2,91 @@
namespace Tests\E2E\Services\Storage;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideConsole;
class StorageConsoleClientTest extends Scope
{
use SideConsole;
use StorageBase;
use ProjectConsole;
use SideClient;
use ProjectCustom;
public function testGetStorageUsage()
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/storage/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/storage/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 3);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['storage']);
$this->assertIsArray($response['body']['files']);
}
public function testGetStorageBucketUsage()
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/storage/default/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals($response['headers']['status-code'], 400);
// TODO: Uncomment once we implement check for missing bucketId in the usage endpoint.
// $response = $this->client->call(Client::METHOD_GET, '/storage/randomBucketId/usage', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id']
// ], $this->getHeaders()), [
// 'range' => '24h'
// ]);
// $this->assertEquals($response['headers']['status-code'], 404);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/storage/default/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 6);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['files.count']);
$this->assertIsArray($response['body']['files.create']);
$this->assertIsArray($response['body']['files.read']);
$this->assertIsArray($response['body']['files.update']);
$this->assertIsArray($response['body']['files.delete']);
}
}

View file

@ -150,6 +150,95 @@ trait UsersBase
return $data;
}
/**
* @depends testGetUser
*/
public function testUpdateUserName(array $data):array
{
/**
* Test for SUCCESS
*/
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/name', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Updated name',
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['name'], 'Updated name');
$user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['name'], 'Updated name');
return $data;
}
/**
* @depends testGetUser
*/
public function testUpdateUserEmail(array $data):array
{
/**
* Test for SUCCESS
*/
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/email', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => 'users.service@updated.com',
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['email'], 'users.service@updated.com');
$user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['email'], 'users.service@updated.com');
return $data;
}
/**
* @depends testUpdateUserEmail
*/
public function testUpdateUserPassword(array $data):array
{
/**
* Test for SUCCESS
*/
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'password' => 'password2',
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertNotEmpty($user['body']['$id']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => 'users.service@updated.com',
'password' => 'password2'
]);
$this->assertEquals($session['headers']['status-code'], 201);
return $data;
}
/**
* @depends testGetUser
*/

View file

@ -0,0 +1,83 @@
<?php
namespace Tests\E2E\Services\Users;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SideConsole;
class UsersConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
public function testGetUsersUsage()
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/users/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h',
'provider' => 'email'
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h',
'provider' => 'some-random-provider'
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/users/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h',
'provider' => 'email'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 9);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['users.count']);
$this->assertIsArray($response['body']['users.create']);
$this->assertIsArray($response['body']['users.read']);
$this->assertIsArray($response['body']['users.update']);
$this->assertIsArray($response['body']['users.delete']);
$this->assertIsArray($response['body']['sessions.create']);
$this->assertIsArray($response['body']['sessions.provider.create']);
$this->assertIsArray($response['body']['sessions.delete']);
$response = $this->client->call(Client::METHOD_GET, '/users/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 9);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['users.count']);
$this->assertIsArray($response['body']['users.create']);
$this->assertIsArray($response['body']['users.read']);
$this->assertIsArray($response['body']['users.update']);
$this->assertIsArray($response['body']['users.delete']);
$this->assertIsArray($response['body']['sessions.create']);
$this->assertIsArray($response['body']['sessions.provider.create']);
$this->assertIsArray($response['body']['sessions.delete']);
}
}