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

Merge branch 'feat-list-endpoints-queries' into feat-variables-api

This commit is contained in:
Matej Baco 2022-08-25 11:14:42 +02:00
commit 8513bb2dbe
85 changed files with 5753 additions and 2025 deletions

3
.env
View file

@ -76,7 +76,8 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
_APP_USAGE_AGGREGATION_INTERVAL=30
_APP_USAGE_TIMESERIES_INTERVAL=2
_APP_USAGE_DATABASE_INTERVAL=15
_APP_USAGE_STATS=enabled
_APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=

View file

@ -234,6 +234,8 @@ ENV _APP_SERVER=swoole \
_APP_SETUP=self-hosted \
_APP_VERSION=$VERSION \
_APP_USAGE_STATS=enabled \
_APP_USAGE_TIMESERIES_INTERVAL=30 \
_APP_USAGE_DATABASE_INTERVAL=900 \
# 14 Days = 1209600 s
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600 \
_APP_MAINTENANCE_RETENTION_AUDIT=1209600 \

View file

@ -52,6 +52,13 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
],
],
'collections' => [
@ -150,6 +157,27 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_documentSecurity'),
'type' => Database::INDEX_KEY,
'attributes' => ['documentSecurity'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -1297,6 +1325,13 @@ $collections = [
]
],
'indexes' => [
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_email'),
'type' => Database::INDEX_UNIQUE,
@ -1311,6 +1346,41 @@ $collections = [
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_passwordUpdate'),
'type' => Database::INDEX_KEY,
'attributes' => ['passwordUpdate'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_registration'),
'type' => Database::INDEX_KEY,
'attributes' => ['registration'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_emailVerification'),
'type' => Database::INDEX_KEY,
'attributes' => ['emailVerification'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_phoneVerification'),
'type' => Database::INDEX_KEY,
'attributes' => ['phoneVerification'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
@ -1750,6 +1820,20 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [128],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_total'),
'type' => Database::INDEX_KEY,
'attributes' => ['total'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -1898,6 +1982,41 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_userId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_teamId'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_invited'),
'type' => Database::INDEX_KEY,
'attributes' => ['invited'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_joined'),
'type' => Database::INDEX_KEY,
'attributes' => ['joined'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_confirm'),
'type' => Database::INDEX_KEY,
'attributes' => ['confirm'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -2046,7 +2165,63 @@ $collections = [
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
]
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_runtime'),
'type' => Database::INDEX_KEY,
'attributes' => ['runtime'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_deployment'),
'type' => Database::INDEX_KEY,
'attributes' => ['deployment'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_schedule'),
'type' => Database::INDEX_KEY,
'attributes' => ['schedule'],
'lengths' => [128],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_scheduleNext'),
'type' => Database::INDEX_KEY,
'attributes' => ['scheduleNext'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_schedulePrevious'),
'type' => Database::INDEX_KEY,
'attributes' => ['schedulePrevious'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_timeout'),
'type' => Database::INDEX_KEY,
'attributes' => ['timeout'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -2199,6 +2374,34 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_entrypoint'),
'type' => Database::INDEX_KEY,
'attributes' => ['entrypoint'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_size'),
'type' => Database::INDEX_KEY,
'attributes' => ['size'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_buildId'),
'type' => Database::INDEX_KEY,
'attributes' => ['buildId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_activate'),
'type' => Database::INDEX_KEY,
'attributes' => ['activate'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -2471,6 +2674,34 @@ $collections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_trigger'),
'type' => Database::INDEX_KEY,
'attributes' => ['trigger'],
'lengths' => [128],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [128],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_statusCode'),
'type' => Database::INDEX_KEY,
'attributes' => ['statusCode'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_time'),
'type' => Database::INDEX_KEY,
'attributes' => ['time'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -2662,6 +2893,48 @@ $collections = [
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [128],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_fileSecurity'),
'type' => Database::INDEX_KEY,
'attributes' => ['fileSecurity'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_maximumFileSize'),
'type' => Database::INDEX_KEY,
'attributes' => ['maximumFileSize'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_encryption'),
'type' => Database::INDEX_KEY,
'attributes' => ['encryption'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_antivirus'),
'type' => Database::INDEX_KEY,
'attributes' => ['antivirus'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
]
],
@ -3064,6 +3337,48 @@ $collections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_signature'),
'type' => Database::INDEX_KEY,
'attributes' => ['signature'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_mimeType'),
'type' => Database::INDEX_KEY,
'attributes' => ['mimeType'],
'lengths' => [127],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_sizeOriginal'),
'type' => Database::INDEX_KEY,
'attributes' => ['sizeOriginal'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_chunksTotal'),
'type' => Database::INDEX_KEY,
'attributes' => ['chunksTotal'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_chunksUploaded'),
'type' => Database::INDEX_KEY,
'attributes' => ['chunksUploaded'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
]
],

View file

@ -170,13 +170,31 @@ return [
],
[
'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.',
'description' => 'Deprecated since 0.16.0, use `_APP_USAGE_TIMESERIES_INTERVAL` and `_APP_USAGE_DATABASE_INTERVAL` instead.',
'introduction' => '0.10.0',
'default' => '30',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_USAGE_TIMESERIES_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to Appwrite Database from Timeseries Database. The default value is 30 seconds.',
'introduction' => '0.16.0',
'default' => '30',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_USAGE_DATABASE_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes.',
'introduction' => '0.16.0',
'default' => '900',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_WORKER_PER_CORE',
'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.',

View file

@ -15,10 +15,12 @@ use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Network\Validator\URL;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
@ -53,6 +55,7 @@ App::post('/v1/account')
->label('auth.type', 'emailPassword')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'create')
@ -69,9 +72,8 @@ App::post('/v1/account')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
@ -130,7 +132,6 @@ App::post('/v1/account')
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString());
$usage->setParam('users.create', 1);
$events->setParam('userId', $user->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -146,6 +147,8 @@ App::post('/v1/account/sessions/email')
->label('auth.type', 'emailPassword')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:email'])
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createEmailSession')
@ -162,9 +165,8 @@ App::post('/v1/account/sessions/email')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('usage')
->inject('events')
->action(function (string $email, string $password, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage, Event $events) {
->action(function (string $email, string $password, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Event $events) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@ -242,12 +244,6 @@ App::post('/v1/account/sessions/email')
->setAttribute('countryName', $countryName)
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.create', 1)
->setParam('provider', 'email')
;
$events
->setParam('userId', $profile->getId())
->setParam('sessionId', $session->getId())
@ -374,6 +370,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->label('docs', false)
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:{request.provider}'])
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
@ -384,8 +382,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('dbForProject')
->inject('geodb')
->inject('events')
->inject('usage')
->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events, Stats $usage) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events) use ($oauthDefaultSuccess) {
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
@ -571,12 +568,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$dbForProject->deleteCachedDocument('users', $user->getId());
$usage
->setParam('users.sessions.create', 1)
->setParam('projectId', $project->getId())
->setParam('provider', 'oauth2-' . $provider)
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
@ -747,6 +738,8 @@ App::put('/v1/account/sessions/magic-url')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:magic-url'])
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateMagicURLSession')
@ -981,6 +974,8 @@ App::put('/v1/account/sessions/phone')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:phone'])
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhoneSession')
@ -1095,6 +1090,8 @@ App::post('/v1/account/sessions/anonymous')
->label('auth.type', 'anonymous')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:anonymous'])
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createAnonymousSession')
@ -1111,9 +1108,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('project')
->inject('dbForProject')
->inject('geodb')
->inject('usage')
->inject('events')
->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Stats $usage, Event $events) {
->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $events) {
$protocol = $request->getProtocol();
@ -1194,11 +1190,6 @@ App::post('/v1/account/sessions/anonymous')
$dbForProject->deleteCachedDocument('users', $user->getId());
$usage
->setParam('users.sessions.create', 1)
->setParam('provider', 'anonymous')
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
@ -1274,6 +1265,7 @@ App::get('/v1/account')
->desc('Get Account')
->groups(['api', 'account'])
->label('scope', 'account')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'get')
@ -1283,9 +1275,8 @@ App::get('/v1/account')
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->inject('response')
->inject('user')
->inject('usage')
->action(function (Response $response, Document $user, Stats $usage) {
$usage->setParam('users.read', 1);
->action(function (Response $response, Document $user) {
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
@ -1293,6 +1284,7 @@ App::get('/v1/account/prefs')
->desc('Get Account Preferences')
->groups(['api', 'account'])
->label('scope', 'account')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getPrefs')
@ -1302,13 +1294,10 @@ App::get('/v1/account/prefs')
->label('sdk.response.model', Response::MODEL_PREFERENCES)
->inject('response')
->inject('user')
->inject('usage')
->action(function (Response $response, Document $user, Stats $usage) {
->action(function (Response $response, Document $user) {
$prefs = $user->getAttribute('prefs', new \stdClass());
$usage->setParam('users.read', 1);
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -1316,6 +1305,7 @@ App::get('/v1/account/sessions')
->desc('Get Account Sessions')
->groups(['api', 'account'])
->label('scope', 'account')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getSessions')
@ -1326,8 +1316,7 @@ App::get('/v1/account/sessions')
->inject('response')
->inject('user')
->inject('locale')
->inject('usage')
->action(function (Response $response, Document $user, Locale $locale, Stats $usage) {
->action(function (Response $response, Document $user, Locale $locale) {
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
@ -1341,8 +1330,6 @@ App::get('/v1/account/sessions')
$sessions[$key] = $session;
}
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
@ -1353,6 +1340,7 @@ App::get('/v1/account/logs')
->desc('Get Account Logs')
->groups(['api', 'account'])
->label('scope', 'account')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getLogs')
@ -1360,15 +1348,19 @@ App::get('/v1/account/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->inject('response')
->inject('user')
->inject('locale')
->inject('geodb')
->inject('dbForProject')
->inject('usage')
->action(function (int $limit, int $offset, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Stats $usage) {
->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Stats $usage) {
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new EventAudit($dbForProject);
@ -1400,8 +1392,6 @@ App::get('/v1/account/logs')
}
}
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output,
@ -1412,6 +1402,7 @@ App::get('/v1/account/sessions/:sessionId')
->desc('Get Session By ID')
->groups(['api', 'account'])
->label('scope', 'account')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getSession')
@ -1424,8 +1415,7 @@ App::get('/v1/account/sessions/:sessionId')
->inject('user')
->inject('locale')
->inject('dbForProject')
->inject('usage')
->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Database $dbForProject, Stats $usage) {
->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Database $dbForProject) {
$sessions = $user->getAttribute('sessions', []);
$sessionId = ($sessionId === 'current')
@ -1441,8 +1431,6 @@ App::get('/v1/account/sessions/:sessionId')
->setAttribute('countryName', $countryName)
;
$usage->setParam('users.read', 1);
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
@ -1456,6 +1444,7 @@ App::patch('/v1/account/name')
->label('event', 'users.[userId].update.name')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateName')
@ -1467,15 +1456,13 @@ App::patch('/v1/account/name')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $name, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $name, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')])));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@ -1488,6 +1475,7 @@ App::patch('/v1/account/password')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePassword')
@ -1500,9 +1488,8 @@ App::patch('/v1/account/password')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Event $events) {
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== null && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
@ -1515,7 +1502,6 @@ App::patch('/v1/account/password')
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now()));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@ -1527,6 +1513,7 @@ App::patch('/v1/account/email')
->label('event', 'users.[userId].update.email')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateEmail')
@ -1539,9 +1526,8 @@ App::patch('/v1/account/email')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
@ -1567,7 +1553,6 @@ App::patch('/v1/account/email')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@ -1579,6 +1564,7 @@ App::patch('/v1/account/phone')
->label('event', 'users.[userId].update.phone')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhone')
@ -1591,9 +1577,8 @@ App::patch('/v1/account/phone')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
@ -1615,7 +1600,6 @@ App::patch('/v1/account/phone')
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@ -1627,6 +1611,7 @@ App::patch('/v1/account/prefs')
->label('event', 'users.[userId].update.prefs')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePrefs')
@ -1638,13 +1623,11 @@ App::patch('/v1/account/prefs')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@ -1656,6 +1639,7 @@ App::patch('/v1/account/status')
->label('event', 'users.[userId].update.status')
->label('scope', 'account')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateStatus')
@ -1668,8 +1652,7 @@ App::patch('/v1/account/status')
->inject('user')
->inject('dbForProject')
->inject('events')
->inject('usage')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Stats $usage) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
@ -1681,8 +1664,6 @@ App::patch('/v1/account/status')
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$usage->setParam('users.delete', 1);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
@ -1692,6 +1673,7 @@ App::delete('/v1/account/sessions/:sessionId')
->label('scope', 'account')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('audits.resource', 'user/{user.$id}')
->label('usage.metric', 'sessions.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteSession')
@ -1706,8 +1688,7 @@ App::delete('/v1/account/sessions/:sessionId')
->inject('dbForProject')
->inject('locale')
->inject('events')
->inject('usage')
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Stats $usage) {
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@ -1749,11 +1730,6 @@ App::delete('/v1/account/sessions/:sessionId')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
$usage
->setParam('users.sessions.delete', 1)
->setParam('users.update', 1)
;
return $response->noContent();
}
}
@ -1768,6 +1744,7 @@ App::patch('/v1/account/sessions/:sessionId')
->label('event', 'users.[userId].sessions.[sessionId].update')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateSession')
@ -1784,8 +1761,7 @@ App::patch('/v1/account/sessions/:sessionId')
->inject('project')
->inject('locale')
->inject('events')
->inject('usage')
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Event $events, Stats $usage) {
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Event $events) {
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
@ -1835,11 +1811,6 @@ App::patch('/v1/account/sessions/:sessionId')
->setPayload($response->output($session, Response::MODEL_SESSION))
;
$usage
->setParam('users.sessions.update', 1)
->setParam('users.update', 1)
;
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
@ -1853,6 +1824,7 @@ App::delete('/v1/account/sessions')
->label('scope', 'account')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('audits.resource', 'user/{user.$id}')
->label('usage.metric', 'sessions.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteSessions')
@ -1866,8 +1838,7 @@ App::delete('/v1/account/sessions')
->inject('dbForProject')
->inject('locale')
->inject('events')
->inject('usage')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Stats $usage) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@ -1905,11 +1876,6 @@ App::delete('/v1/account/sessions')
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
$usage
->setParam('users.sessions.delete', $numOfSessions)
->setParam('users.update', 1)
;
$response->noContent();
});
@ -1920,6 +1886,7 @@ App::post('/v1/account/recovery')
->label('event', 'users.[userId].recovery.[tokenId].create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createRecovery')
@ -1938,8 +1905,7 @@ App::post('/v1/account/recovery')
->inject('locale')
->inject('mails')
->inject('events')
->inject('usage')
->action(function (string $email, string $url, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Event $events, Stats $usage) {
->action(function (string $email, string $url, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Event $events) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@ -2014,8 +1980,6 @@ App::post('/v1/account/recovery')
// Hide secret for clients
$recovery->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
$usage->setParam('users.update', 1);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($recovery, Response::MODEL_TOKEN);
});
@ -2027,6 +1991,7 @@ App::put('/v1/account/recovery')
->label('event', 'users.[userId].recovery.[tokenId].update')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateRecovery')
@ -2042,9 +2007,8 @@ App::put('/v1/account/recovery')
->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Event $events) {
if ($password !== $passwordAgain) {
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
}
@ -2080,8 +2044,6 @@ App::put('/v1/account/recovery')
$dbForProject->deleteDocument('tokens', $recovery);
$dbForProject->deleteCachedDocument('users', $profile->getId());
$usage->setParam('users.update', 1);
$events
->setParam('userId', $profile->getId())
->setParam('tokenId', $recoveryDocument->getId())
@ -2096,6 +2058,7 @@ App::post('/v1/account/verification')
->label('scope', 'account')
->label('event', 'users.[userId].verification.[tokenId].create')
->label('audits.resource', 'user/{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createVerification')
@ -2114,8 +2077,7 @@ App::post('/v1/account/verification')
->inject('locale')
->inject('events')
->inject('mails')
->inject('usage')
->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $events, Mail $mails, Stats $usage) {
->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $events, Mail $mails) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@ -2174,8 +2136,6 @@ App::post('/v1/account/verification')
// Hide secret for clients
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
$usage->setParam('users.update', 1);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($verification, Response::MODEL_TOKEN);
});
@ -2186,6 +2146,7 @@ App::put('/v1/account/verification')
->label('scope', 'public')
->label('event', 'users.[userId].verification.[tokenId].update')
->label('audits.resource', 'user/{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateVerification')
@ -2200,9 +2161,8 @@ App::put('/v1/account/verification')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $events) {
$profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
@ -2230,8 +2190,6 @@ App::put('/v1/account/verification')
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteCachedDocument('users', $profile->getId());
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verificationDocument->getId())
@ -2246,6 +2204,7 @@ App::post('/v1/account/verification/phone')
->label('scope', 'account')
->label('event', 'users.[userId].verification.[tokenId].create')
->label('audits.resource', 'user/{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPhoneVerification')
@ -2260,9 +2219,8 @@ App::post('/v1/account/verification/phone')
->inject('user')
->inject('dbForProject')
->inject('events')
->inject('usage')
->inject('messaging')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Stats $usage, EventPhone $messaging) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, EventPhone $messaging) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
@ -2319,8 +2277,6 @@ App::post('/v1/account/verification/phone')
// Hide secret for clients
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
$usage->setParam('users.update', 1);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($verification, Response::MODEL_TOKEN);
});
@ -2331,6 +2287,7 @@ App::put('/v1/account/verification/phone')
->label('scope', 'public')
->label('event', 'users.[userId].verification.[tokenId].update')
->label('audits.resource', 'user/{response.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhoneVerification')
@ -2345,9 +2302,8 @@ App::put('/v1/account/verification/phone')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $events) {
$profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
@ -2373,8 +2329,6 @@ App::put('/v1/account/verification/phone')
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteCachedDocument('users', $profile->getId());
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verificationDocument->getId())

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Database\Validator\UID;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
@ -22,6 +22,9 @@ use Utopia\Storage\Validator\Upload;
use Appwrite\Utopia\Response;
use Utopia\Swoole\Request;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Database\Validator\Queries\Functions;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -102,38 +105,39 @@ App::get('/v1/functions')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION_LIST)
->param('queries', [], new Functions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Functions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of functions to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the function used as the starting point for the query, excluding the function itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ' . Database::ORDER_ASC . ' or ' . Database::ORDER_DESC . ' order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('functions', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$functionId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('functions', $functionId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Function '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Function '{$functionId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$response->dynamic(new Document([
'functions' => $dbForProject->find('functions', \array_merge($filterQueries, $queries)),
'functions' => $dbForProject->find('functions', $queries),
'total' => $dbForProject->count('functions', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_FUNCTION_LIST);
});
@ -195,7 +199,7 @@ App::get('/v1/functions/:functionId/usage')
->label('scope', 'functions.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'getUsage')
->label('sdk.method', 'getFunctionUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
@ -233,9 +237,14 @@ App::get('/v1/functions/:functionId/usage')
];
$metrics = [
"functions.$functionId.executions",
"functions.$functionId.failures",
"functions.$functionId.compute"
"executions.$functionId.compute.total",
"executions.$functionId.compute.success",
"executions.$functionId.compute.failure",
"executions.$functionId.compute.time",
"builds.$functionId.compute.total",
"builds.$functionId.compute.success",
"builds.$functionId.compute.failure",
"builds.$functionId.compute.time",
];
$stats = [];
@ -280,9 +289,115 @@ App::get('/v1/functions/:functionId/usage')
$usage = new Document([
'range' => $range,
'functionsExecutions' => $stats["functions.$functionId.executions"],
'functionsFailures' => $stats["functions.$functionId.failures"],
'functionsCompute' => $stats["functions.$functionId.compute"]
'executionsTotal' => $stats["executions.$functionId.compute.total"] ?? [],
'executionsFailure' => $stats["executions.$functionId.compute.failure"] ?? [],
'executionsSuccesse' => $stats["executions.$functionId.compute.success"] ?? [],
'executionsTime' => $stats["executions.$functionId.compute.time"] ?? [],
'buildsTotal' => $stats["builds.$functionId.compute.total"] ?? [],
'buildsFailure' => $stats["builds.$functionId.compute.failure"] ?? [],
'buildsSuccess' => $stats["builds.$functionId.compute.success"] ?? [],
'buildsTime' => $stats["builds.$functionId.compute.time" ?? []]
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTION);
});
App::get('/v1/functions/usage')
->desc('Get Functions Usage')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->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('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $range, Response $response, Database $dbForProject) {
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
],
'7d' => [
'period' => '1d',
'limit' => 7,
],
'30d' => [
'period' => '1d',
'limit' => 30,
],
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$metrics = [
'executions.$all.compute.total',
'executions.$all.compute.failure',
'executions.$all.compute.success',
'executions.$all.compute.time',
'builds.$all.compute.total',
'builds.$all.compute.failure',
'builds.$all.compute.success',
'builds.$all.compute.time',
];
$stats = [];
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'executionsTotal' => $stats[$metrics[0]] ?? [],
'executionsFailure' => $stats[$metrics[1]] ?? [],
'executionsSuccess' => $stats[$metrics[2]] ?? [],
'executionsTime' => $stats[$metrics[3]] ?? [],
'buildsTotal' => $stats[$metrics[4]] ?? [],
'buildsFailure' => $stats[$metrics[5]] ?? [],
'buildsSuccess' => $stats[$metrics[6]] ?? [],
'buildsTime' => $stats[$metrics[7]] ?? [],
]);
}
@ -478,12 +593,11 @@ App::post('/v1/functions/:functionId/deployments')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->inject('project')
->inject('deviceFunctions')
->inject('deviceLocal')
->action(function (string $functionId, string $entrypoint, mixed $code, bool $activate, Request $request, Response $response, Database $dbForProject, Stats $usage, Event $events, Document $project, Device $deviceFunctions, Device $deviceLocal) {
->action(function (string $functionId, string $entrypoint, mixed $code, bool $activate, Request $request, Response $response, Database $dbForProject, Event $events, Document $project, Device $deviceFunctions, Device $deviceLocal) {
$function = $dbForProject->getDocument('functions', $functionId);
@ -610,8 +724,6 @@ App::post('/v1/functions/:functionId/deployments')
->setDeployment($deployment)
->setProject($project)
->trigger();
$usage->setParam('storage', $deployment->getAttribute('size', 0));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
@ -659,15 +771,11 @@ App::get('/v1/functions/:functionId/deployments')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DEPLOYMENT_LIST)
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of deployments to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the deployment used as the starting point for the query, excluding the deployment itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ' . Database::ORDER_ASC . ' or ' . Database::ORDER_DESC . ' order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $functionId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) {
$function = $dbForProject->getDocument('functions', $functionId);
@ -675,30 +783,36 @@ App::get('/v1/functions/:functionId/deployments')
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$filterQueries[] = Query::equal('resourceId', [$function->getId()]);
$filterQueries[] = Query::equal('resourceType', ['functions']);
// Set default limit
$queries[] = Query::limit(25);
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('deployments', $cursor);
// Set resource queries
$queries[] = Query::equal('resourceId', [$function->getId()]);
$queries[] = Query::equal('resourceType', ['functions']);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$deploymentId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('deployments', $deploymentId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Tag '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$results = $dbForProject->find('deployments', \array_merge($filterQueries, $queries));
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
foreach ($results as $result) {
@ -766,11 +880,10 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('deletes')
->inject('events')
->inject('deviceFunctions')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Stats $usage, Delete $deletes, Event $events, Device $deviceFunctions) {
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $deletes, Event $events, Device $deviceFunctions) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -798,9 +911,6 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
])));
}
$usage
->setParam('storage', $deployment->getAttribute('size', 0) * -1);
$events
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
@ -993,11 +1103,12 @@ App::post('/v1/functions/:functionId/executions')
Authorization::skip(fn () => $dbForProject->updateDocument('executions', $executionId, $execution));
// TODO revise this later using route label
$usage
->setParam('functionId', $function->getId())
->setParam('functionExecution', 1)
->setParam('functionStatus', $execution->getAttribute('status', ''))
->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000); // ms
->setParam('functionId', $function->getId())
->setParam('executions.{scope}.compute', 1)
->setParam('executionStatus', $execution->getAttribute('status', ''))
->setParam('executionTime', $execution->getAttribute('time')); // ms
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
@ -1024,14 +1135,11 @@ App::get('/v1/functions/:functionId/executions')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_EXECUTION_LIST)
->param('functionId', '', new UID(), 'Function ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of executions to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('cursor', '', new UID(), 'ID of the execution used as the starting point for the query, excluding the execution itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $functionId, int $limit, int $offset, string $search, string $cursor, string $cursorDirection, Response $response, Database $dbForProject) {
->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) {
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
@ -1039,29 +1147,35 @@ App::get('/v1/functions/:functionId/executions')
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$filterQueries = [
Query::equal('functionId', [$function->getId()])
];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('executions', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Set internal queries
$queries[] = Query::equal('functionId', [$function->getId()]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$executionId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('executions', $executionId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Tag '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$results = $dbForProject->find('executions', \array_merge($filterQueries, $queries));
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$roles = Authorization::getRoles();

View file

@ -291,13 +291,13 @@ App::get('/v1/projects/:projectId/usage')
$dbForProject->setNamespace("_{$project->getInternalId()}");
$metrics = [
'requests',
'network',
'executions',
'users.count',
'databases.documents.count',
'databases.collections.count',
'storage.total'
'project.$all.network.requests',
'project.$all.network.bandwidth',
'project.$all.storage.size',
'users.$all.count.total',
'collections.$all.count.total',
'documents.$all.count.total',
'executions.$all.compute.total',
];
$stats = [];
@ -342,13 +342,13 @@ App::get('/v1/projects/:projectId/usage')
$usage = new Document([
'range' => $range,
'requests' => $stats['requests'],
'network' => $stats['network'],
'functions' => $stats['executions'],
'documents' => $stats['databases.documents.count'],
'collections' => $stats['databases.collections.count'],
'users' => $stats['users.count'],
'storage' => $stats['storage.total']
'requests' => $stats[$metrics[0]] ?? [],
'network' => $stats[$metrics[1]] ?? [],
'storage' => $stats[$metrics[2]] ?? [],
'users' => $stats[$metrics[3]] ?? [],
'collections' => $stats[$metrics[4]] ?? [],
'documents' => $stats[$metrics[5]] ?? [],
'executions' => $stats[$metrics[6]] ?? [],
]);
}

View file

@ -6,7 +6,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
@ -24,6 +24,8 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
use Utopia\Image\Image;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Storage\Device;
@ -48,6 +50,7 @@ App::post('/v1/storage/buckets')
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].create')
->label('audits.resource', 'buckets/{response.$id}')
->label('usage.metric', 'buckets.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createBucket')
@ -66,9 +69,8 @@ App::post('/v1/storage/buckets')
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $events) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
@ -133,8 +135,6 @@ App::post('/v1/storage/buckets')
->setParam('bucketId', $bucket->getId())
;
$usage->setParam('storage.buckets.create', 1);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
@ -143,6 +143,7 @@ App::get('/v1/storage/buckets')
->desc('List buckets')
->groups(['api', 'storage'])
->label('scope', 'buckets.read')
->label('usage.metric', 'buckets.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'listBuckets')
@ -150,41 +151,42 @@ App::get('/v1/storage/buckets')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BUCKET_LIST)
->param('queries', [], new Buckets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Buckets::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the bucket used as the starting point for the query, excluding the bucket itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ' . Database::ORDER_ASC . ' or ' . Database::ORDER_DESC . ' order.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage) {
->action(function (array $queries, string $search, Response $response, Database $dbForProject, Stats $usage) {
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('name', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('buckets', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$bucketId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('buckets', $bucketId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Bucket '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Bucket '{$bucketId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$usage->setParam('storage.buckets.read', 1);
$response->dynamic(new Document([
'buckets' => $dbForProject->find('buckets', \array_merge($filterQueries, $queries)),
'buckets' => $dbForProject->find('buckets', $queries),
'total' => $dbForProject->count('buckets', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_BUCKET_LIST);
});
@ -193,6 +195,7 @@ App::get('/v1/storage/buckets/:bucketId')
->desc('Get Bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.read')
->label('usage.metric', 'buckets.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getBucket')
@ -203,8 +206,7 @@ App::get('/v1/storage/buckets/:bucketId')
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function (string $bucketId, Response $response, Database $dbForProject, Stats $usage) {
->action(function (string $bucketId, Response $response, Database $dbForProject) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@ -212,8 +214,6 @@ App::get('/v1/storage/buckets/:bucketId')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$usage->setParam('storage.buckets.read', 1);
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
@ -223,6 +223,7 @@ App::put('/v1/storage/buckets/:bucketId')
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].update')
->label('audits.resource', 'buckets/{response.$id}')
->label('usage.metric', 'buckets.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateBucket')
@ -241,9 +242,8 @@ App::put('/v1/storage/buckets/:bucketId')
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $events) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@ -278,8 +278,6 @@ App::put('/v1/storage/buckets/:bucketId')
->setParam('bucketId', $bucket->getId())
;
$usage->setParam('storage.buckets.update', 1);
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
@ -289,6 +287,7 @@ App::delete('/v1/storage/buckets/:bucketId')
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].delete')
->label('audits.resource', 'buckets/{request.bucketId}')
->label('usage.metric', 'buckets.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteBucket')
@ -300,8 +299,7 @@ App::delete('/v1/storage/buckets/:bucketId')
->inject('dbForProject')
->inject('deletes')
->inject('events')
->inject('usage')
->action(function (string $bucketId, Response $response, Database $dbForProject, Delete $deletes, Event $events, Stats $usage) {
->action(function (string $bucketId, Response $response, Database $dbForProject, Delete $deletes, Event $events) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@ -321,8 +319,6 @@ App::delete('/v1/storage/buckets/:bucketId')
->setPayload($response->output($bucket, Response::MODEL_BUCKET))
;
$usage->setParam('storage.buckets.delete', 1);
$response->noContent();
});
@ -333,6 +329,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].create')
->label('audits.resource', 'files/{response.$id}')
->label('usage.metric', 'files.{scope}.requests.create')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createFile')
@ -350,13 +348,12 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('usage')
->inject('events')
->inject('mode')
->inject('deviceFiles')
->inject('deviceLocal')
->inject('deletes')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -403,7 +400,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
@ -585,12 +582,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
}
$usage
->setParam('storage', $sizeActual ?? 0)
->setParam('storage.files.create', 1)
->setParam('bucketId', $bucketId)
;
} else {
try {
if ($file->isEmpty()) {
@ -650,6 +641,8 @@ App::get('/v1/storage/buckets/:bucketId/files')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('usage.metric', 'files.{scope}.requests.read')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'listFiles')
->label('sdk.description', '/docs/references/storage/list-files.md')
@ -657,17 +650,12 @@ App::get('/v1/storage/buckets/:bucketId/files')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE_LIST)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('queries', [], new Files(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Files::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of files to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the file used as the starting point for the query, excluding the file itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ' . Database::ORDER_ASC . ' or ' . Database::ORDER_DESC . ' order.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->action(function (string $bucketId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage, string $mode) {
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, Stats $usage, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -682,41 +670,44 @@ App::get('/v1/storage/buckets/:bucketId/files')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('name', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $fileSecurity
? $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor)
: Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor));
// Set default limit
$queries[] = Query::limit(25);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$cursor}' for the 'cursor' value not found.");
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$fileId = $cursor->getValue();
if ($fileSecurity && !$valid) {
$cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$fileId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
if ($fileSecurity && !$valid) {
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries));
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries)));
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
$response->dynamic(new Document([
'files' => $files,
'total' => $total,
@ -729,6 +720,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('usage.metric', 'files.{scope}.requests.read')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFile')
->label('sdk.description', '/docs/references/storage/get-file.md')
@ -739,9 +732,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) {
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -766,11 +758,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
$response->dynamic($file, Response::MODEL_FILE);
});
@ -781,6 +768,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->label('scope', 'files.read')
->label('cache', true)
->label('cache.resource', 'file/{request.fileId}')
->label('usage.metric', 'files.{scope}.requests.read')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFilePreview')
@ -805,11 +794,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->inject('deviceLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles, Device $deviceLocal) {
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceFiles, Device $deviceLocal) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@ -926,11 +914,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$data = $image->output($output, $quality);
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
$response
@ -938,6 +921,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->setContentType($contentType)
->file($data)
;
unset($image);
});
@ -946,6 +930,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->desc('Get File for Download')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('usage.metric', 'files.{scope}.requests.read')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFileDownload')
@ -958,10 +944,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles) {
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -992,11 +977,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
$response
->setContentType($file->getAttribute('mimeType'))
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
@ -1081,6 +1061,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->desc('Get File for View')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('usage.metric', 'files.{scope}.requests.read')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFileView')
@ -1093,10 +1075,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles) {
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -1188,11 +1169,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
$source = $compressor->decompress($source);
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
if (!empty($source)) {
if (!empty($rangeHeader)) {
$response->send(substr($source, $start, ($end - $start + 1)));
@ -1229,6 +1205,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].update')
->label('audits.resource', 'files/{response.$id}')
->label('usage.metric', 'files.{scope}.requests.update')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateFile')
@ -1242,10 +1220,9 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('usage')
->inject('mode')
->inject('events')
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, string $mode, Event $events) {
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -1260,16 +1237,24 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
// Read permission should not be required for update
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
if (\is_null($permissions)) {
$permissions = $file->getPermissions() ?? [];
}
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
@ -1285,19 +1270,12 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
$file->setAttribute('$permissions', $permissions);
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
@ -1308,11 +1286,6 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->setContext('bucket', $bucket)
;
$usage
->setParam('storage.files.update', 1)
->setParam('bucketId', $bucketId)
;
$response->dynamic($file, Response::MODEL_FILE);
});
@ -1323,6 +1296,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].delete')
->label('audits.resource', 'file/{request.fileId}')
->label('usage.metric', 'files.{scope}.requests.delete')
->label('usage.params', ['bucketId:{request.bucketId}'])
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteFile')
@ -1334,11 +1309,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->inject('deletes')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Delete $deletes) {
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, string $mode, Device $deviceFiles, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
@ -1346,17 +1320,14 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
}
$fileSecurity = $bucket->getAttributes('fileSecurity', false);
$validator = new Authorization('delete');
$validator = new Authorization(Database::PERMISSION_DELETE);
$valid = $validator->isValid($bucket->getDelete());
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
// Read permission should not be required for delete
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
@ -1378,7 +1349,11 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->setResource('file/' . $fileId)
;
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($fileSecurity && !$valid) {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$deleted = Authorization::skip(fn() => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if (!$deleted) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove file from DB');
@ -1387,12 +1362,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to delete file from device');
}
$usage
->setParam('storage', $file->getAttribute('size', 0) * -1)
->setParam('storage.files.delete', 1)
->setParam('bucketId', $bucketId)
;
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
@ -1440,18 +1409,18 @@ App::get('/v1/storage/usage')
];
$metrics = [
"storage.deployments.total",
"storage.files.total",
"storage.files.count",
"storage.buckets.count",
"storage.buckets.create",
"storage.buckets.read",
"storage.buckets.update",
"storage.buckets.delete",
"storage.files.create",
"storage.files.read",
"storage.files.update",
"storage.files.delete",
'project.$all.storage.size',
'buckets.$all.count.total',
'buckets.$all.requests.create',
'buckets.$all.requests.read',
'buckets.$all.requests.update',
'buckets.$all.requests.delete',
'files.$all.storage.size',
'files.$all.count.total',
'files.$all.requests.create',
'files.$all.requests.read',
'files.$all.requests.update',
'files.$all.requests.delete',
];
$stats = [];
@ -1496,18 +1465,17 @@ App::get('/v1/storage/usage')
$usage = new Document([
'range' => $range,
'filesStorage' => $stats['storage.files.total'],
'deploymentsStorage' => $stats['storage.deployments.total'],
'filesCount' => $stats['storage.files.count'],
'bucketsCount' => $stats['storage.buckets.count'],
'bucketsCreate' => $stats['storage.buckets.create'],
'bucketsRead' => $stats['storage.buckets.read'],
'bucketsUpdate' => $stats['storage.buckets.update'],
'bucketsDelete' => $stats['storage.buckets.delete'],
'filesCreate' => $stats['storage.files.create'],
'filesRead' => $stats['storage.files.read'],
'filesUpdate' => $stats['storage.files.update'],
'filesDelete' => $stats['storage.files.delete'],
'bucketsCount' => $stats['buckets.$all.count.total'],
'bucketsCreate' => $stats['buckets.$all.requests.create'],
'bucketsRead' => $stats['buckets.$all.requests.read'],
'bucketsUpdate' => $stats['buckets.$all.requests.update'],
'bucketsDelete' => $stats['buckets.$all.requests.delete'],
'storage' => $stats['project.$all.storage.size'],
'filesCount' => $stats['files.$all.count.total'],
'filesCreate' => $stats['files.$all.requests.create'],
'filesRead' => $stats['files.$all.requests.read'],
'filesUpdate' => $stats['files.$all.requests.update'],
'filesDelete' => $stats['files.$all.requests.delete'],
]);
}
@ -1558,12 +1526,12 @@ App::get('/v1/storage/:bucketId/usage')
];
$metrics = [
"storage.buckets.$bucketId.files.count",
"storage.buckets.$bucketId.files.total",
"storage.buckets.$bucketId.files.create",
"storage.buckets.$bucketId.files.read",
"storage.buckets.$bucketId.files.update",
"storage.buckets.$bucketId.files.delete",
"files.{$bucketId}.count.total",
"files.{$bucketId}.storage.size",
"files.{$bucketId}.requests.create",
"files.{$bucketId}.requests.read",
"files.{$bucketId}.requests.update",
"files.{$bucketId}.requests.delete",
];
$stats = [];
@ -1607,12 +1575,12 @@ App::get('/v1/storage/:bucketId/usage')
$usage = new Document([
'range' => $range,
'filesStorage' => $stats["storage.buckets.$bucketId.files.total"],
'filesCount' => $stats["storage.buckets.$bucketId.files.count"],
'filesCreate' => $stats["storage.buckets.$bucketId.files.create"],
'filesRead' => $stats["storage.buckets.$bucketId.files.read"],
'filesUpdate' => $stats["storage.buckets.$bucketId.files.update"],
'filesDelete' => $stats["storage.buckets.$bucketId.files.delete"],
'filesCount' => $stats[$metrics[0]],
'filesStorage' => $stats[$metrics[1]],
'filesCreate' => $stats[$metrics[2]],
'filesRead' => $stats[$metrics[3]],
'filesUpdate' => $stats[$metrics[4]],
'filesDelete' => $stats[$metrics[5]],
]);
}

View file

@ -10,6 +10,11 @@ use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
@ -122,37 +127,38 @@ App::get('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->param('queries', [], new Teams(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Teams::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of teams to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the team used as the starting point for the query, excluding the team itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('teams', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$teamId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('teams', $teamId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Team '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Team '{$teamId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$results = $dbForProject->find('teams', \array_merge($filterQueries, $queries));
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('teams', $queries);
$total = $dbForProject->count('teams', $filterQueries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
@ -460,15 +466,11 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of memberships to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the membership used as the starting point for the query, excluding the membership itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ' . Database::ORDER_ASC . ' or ' . Database::ORDER_DESC . ' order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $teamId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (string $teamId, array $queries, string $search, Response $response, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -476,29 +478,37 @@ App::get('/v1/teams/:teamId/memberships')
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$filterQueries = [Query::equal('teamId', [$teamId])];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$otherQueries = [];
$otherQueries[] = Query::limit($limit);
$otherQueries[] = Query::offset($offset);
$otherQueries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('memberships', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Set internal queries
$queries[] = Query::equal('teamId', [$teamId]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$membershipId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('memberships', $membershipId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Membership '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Membership '{$membershipId}' for the 'cursor' value not found.");
}
$otherQueries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$memberships = $dbForProject->find(
collection: 'memberships',
queries: \array_merge($filterQueries, $otherQueries),
queries: $queries,
);
$total = $dbForProject->count(
@ -847,18 +857,12 @@ App::get('/v1/teams/:teamId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('teamId', null, new UID(), 'Team ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function ($teamId, $limit, $offset, $response, $dbForProject, $locale, $geodb) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -866,6 +870,11 @@ App::get('/v1/teams/:teamId/logs')
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $limit, $offset);

View file

@ -7,8 +7,11 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
@ -28,13 +31,12 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Stats $usage, Event $events): Document
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Event $events): Document
{
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
@ -76,8 +78,6 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
$usage->setParam('users.create', 1);
$events->setParam('userId', $user->getId());
return $user;
@ -89,6 +89,7 @@ App::post('/v1/users')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'create')
@ -103,10 +104,9 @@ App::post('/v1/users')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $usage, $events);
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -118,6 +118,7 @@ App::post('/v1/users/bcrypt')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createBcryptUser')
@ -131,10 +132,9 @@ App::post('/v1/users/bcrypt')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -146,6 +146,7 @@ App::post('/v1/users/md5')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createMD5User')
@ -159,10 +160,9 @@ App::post('/v1/users/md5')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -174,6 +174,7 @@ App::post('/v1/users/argon2')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createArgon2User')
@ -187,10 +188,9 @@ App::post('/v1/users/argon2')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -202,6 +202,7 @@ App::post('/v1/users/sha')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSHAUser')
@ -216,16 +217,15 @@ App::post('/v1/users/sha')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Event $events) {
$options = '{}';
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -237,6 +237,7 @@ App::post('/v1/users/phpass')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createPHPassUser')
@ -250,10 +251,9 @@ App::post('/v1/users/phpass')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -265,6 +265,7 @@ App::post('/v1/users/scrypt')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptUser')
@ -283,9 +284,8 @@ App::post('/v1/users/scrypt')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Event $events) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
@ -294,7 +294,7 @@ App::post('/v1/users/scrypt')
'length' => $passwordLength
];
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -306,6 +306,7 @@ App::post('/v1/users/scrypt-modified')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptModifiedUser')
@ -322,10 +323,9 @@ App::post('/v1/users/scrypt-modified')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -335,6 +335,7 @@ App::get('/v1/users')
->desc('List Users')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'list')
@ -342,41 +343,42 @@ App::get('/v1/users')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER_LIST)
->param('queries', [], new Users(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of users to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the user used as the starting point for the query, excluding the user itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', Database::ORDER_ASC, new WhiteList([Database::ORDER_ASC, Database::ORDER_DESC], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage) {
->action(function (array $queries, string $search, Response $response, Database $dbForProject, Stats $usage) {
$filterQueries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$filterQueries[] = Query::search('search', $search);
$queries[] = Query::search('search', $search);
}
$queries = [];
$queries[] = Query::limit($limit);
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
$cursorDocument = $dbForProject->getDocument('users', $cursor);
// Set default limit
$queries[] = Query::limit(25);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)[0] ?? null;
if ($cursor !== null) {
/** @var Query $cursor */
$userId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('users', $userId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$cursor}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$userId}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'users' => $dbForProject->find('users', \array_merge($filterQueries, $queries)),
'users' => $dbForProject->find('users', $queries),
'total' => $dbForProject->count('users', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_USER_LIST);
});
@ -385,6 +387,7 @@ App::get('/v1/users/:userId')
->desc('Get User')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'get')
@ -395,41 +398,7 @@ App::get('/v1/users/:userId')
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function (string $userId, Response $response, Database $dbForProject, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$usage->setParam('users.read', 1);
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/prefs')
->desc('Update User Preferences')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.prefs')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePrefs')
->label('sdk.description', '/docs/references/users/update-user-prefs.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PREFERENCES)
->param('userId', '', new UID(), 'User ID.')
->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
@ -437,19 +406,14 @@ App::patch('/v1/users/:userId/prefs')
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
$response->dynamic($user, Response::MODEL_USER);
});
App::get('/v1/users/:userId/prefs')
->desc('Get User Preferences')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getPrefs')
@ -460,8 +424,7 @@ App::get('/v1/users/:userId/prefs')
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function (string $userId, Response $response, Database $dbForProject, Stats $usage) {
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
@ -471,8 +434,6 @@ 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);
});
@ -480,6 +441,7 @@ App::get('/v1/users/:userId/sessions')
->desc('Get User Sessions')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getSessions')
@ -491,8 +453,7 @@ App::get('/v1/users/:userId/sessions')
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('usage')
->action(function (string $userId, Response $response, Database $dbForProject, Locale $locale, Stats $usage) {
->action(function (string $userId, Response $response, Database $dbForProject, Locale $locale) {
$user = $dbForProject->getDocument('users', $userId);
@ -512,8 +473,6 @@ App::get('/v1/users/:userId/sessions')
$sessions[$key] = $session;
}
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
@ -524,6 +483,7 @@ App::get('/v1/users/:userId/memberships')
->desc('Get User Memberships')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getMemberships')
@ -563,6 +523,7 @@ App::get('/v1/users/:userId/logs')
->desc('Get User Logs')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getLogs')
@ -571,14 +532,13 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('usage')
->action(function (string $userId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) {
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
@ -586,6 +546,11 @@ App::get('/v1/users/:userId/logs')
throw new Exception(Exception::USER_NOT_FOUND);
}
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
@ -631,8 +596,6 @@ App::get('/v1/users/:userId/logs')
}
}
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output,
@ -646,6 +609,7 @@ App::patch('/v1/users/:userId/status')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateStatus')
@ -657,9 +621,8 @@ 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('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $status, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, bool $status, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -669,9 +632,8 @@ App::patch('/v1/users/:userId/status')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$events
->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -682,6 +644,7 @@ App::patch('/v1/users/:userId/verification')
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmailVerification')
@ -693,9 +656,8 @@ App::patch('/v1/users/:userId/verification')
->param('emailVerification', false, new Boolean(), 'User email verification status.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -705,9 +667,8 @@ App::patch('/v1/users/:userId/verification')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$events
->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -718,6 +679,7 @@ App::patch('/v1/users/:userId/verification/phone')
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhoneVerification')
@ -729,9 +691,8 @@ App::patch('/v1/users/:userId/verification/phone')
->param('phoneVerification', false, new Boolean(), 'User phone verification status.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $phoneVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, bool $phoneVerification, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -741,9 +702,8 @@ App::patch('/v1/users/:userId/verification/phone')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$events
->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -755,6 +715,7 @@ App::patch('/v1/users/:userId/name')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateName')
@ -777,8 +738,7 @@ App::patch('/v1/users/:userId/name')
$user
->setAttribute('name', $name)
->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')]));
;
->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')]));;
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -794,6 +754,7 @@ App::patch('/v1/users/:userId/password')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePassword')
@ -834,6 +795,7 @@ App::patch('/v1/users/:userId/email')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmail')
@ -859,8 +821,7 @@ App::patch('/v1/users/:userId/email')
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')]))
;
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -879,6 +840,7 @@ App::patch('/v1/users/:userId/phone')
->label('event', 'users.[userId].update.phone')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhone')
@ -902,8 +864,7 @@ App::patch('/v1/users/:userId/phone')
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number]));
;
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number]));;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -923,6 +884,7 @@ App::patch('/v1/users/:userId/verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmailVerification')
@ -934,9 +896,8 @@ App::patch('/v1/users/:userId/verification')
->param('emailVerification', false, new Boolean(), 'User email verification status.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -946,8 +907,6 @@ App::patch('/v1/users/:userId/verification')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
@ -958,6 +917,7 @@ App::patch('/v1/users/:userId/prefs')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.prefs')
->label('scope', 'users.write')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePrefs')
@ -969,9 +929,8 @@ App::patch('/v1/users/:userId/prefs')
->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -981,9 +940,8 @@ App::patch('/v1/users/:userId/prefs')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$events
->setParam('userId', $user->getId());
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -994,6 +952,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'sessions.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteSession')
@ -1005,8 +964,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('usage')
->action(function (string $userId, string $sessionId, Response $response, Database $dbForProject, Event $events, Stats $usage) {
->action(function (string $userId, string $sessionId, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -1023,16 +981,9 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
$dbForProject->deleteDocument('sessions', $session->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $sessionId)
;
->setParam('sessionId', $sessionId);
$response->noContent();
});
@ -1043,6 +994,7 @@ App::delete('/v1/users/:userId/sessions')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{user.$id}')
->label('usage.metric', 'sessions.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteSessions')
@ -1053,8 +1005,7 @@ App::delete('/v1/users/:userId/sessions')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('usage')
->action(function (string $userId, Response $response, Database $dbForProject, Event $events, Stats $usage) {
->action(function (string $userId, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -1064,7 +1015,8 @@ App::delete('/v1/users/:userId/sessions')
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) { /** @var Document $session */
foreach ($sessions as $key => $session) {
/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
//TODO: fix this
}
@ -1073,13 +1025,7 @@ App::delete('/v1/users/:userId/sessions')
$events
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_USER))
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
->setPayload($response->output($user, Response::MODEL_USER));
$response->noContent();
});
@ -1090,6 +1036,7 @@ App::delete('/v1/users/:userId')
->label('event', 'users.[userId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'users.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'delete')
@ -1101,8 +1048,7 @@ App::delete('/v1/users/:userId')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('usage')
->action(function (string $userId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) {
->action(function (string $userId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
$user = $dbForProject->getDocument('users', $userId);
@ -1117,15 +1063,11 @@ App::delete('/v1/users/:userId')
$deletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone)
;
->setDocument($clone);
$events
->setParam('userId', $user->getId())
->setPayload($response->output($clone, Response::MODEL_USER))
;
$usage->setParam('users.delete', 1);
->setPayload($response->output($clone, Response::MODEL_USER));
$response->noContent();
});
@ -1141,7 +1083,7 @@ App::get('/v1/users/usage')
->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(fn($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
@ -1169,14 +1111,14 @@ App::get('/v1/users/usage')
];
$metrics = [
"users.count",
"users.create",
"users.read",
"users.update",
"users.delete",
"users.sessions.create",
"users.sessions.$provider.create",
"users.sessions.delete"
'users.$all.requests.count',
'users.$all.requests.create',
'users.$all.requests.read',
'users.$all.requests.update',
'users.$all.requests.delete',
'sessions.$all.requests.create',
'sessions.$all.requests.delete',
"sessions.$provider.requests.create",
];
$stats = [];
@ -1221,14 +1163,14 @@ App::get('/v1/users/usage')
$usage = new Document([
'range' => $range,
'usersCount' => $stats["users.count"],
'usersCreate' => $stats["users.create"],
'usersRead' => $stats["users.read"],
'usersUpdate' => $stats["users.update"],
'usersDelete' => $stats["users.delete"],
'sessionsCreate' => $stats["users.sessions.create"],
'sessionsProviderCreate' => $stats["users.sessions.$provider.create"],
'sessionsDelete' => $stats["users.sessions.delete"]
'usersCount' => $stats['users.$all.requests.count'] ?? [],
'usersCreate' => $stats['users.$all.requests.create'] ?? [],
'usersRead' => $stats['users.$all.requests.read'] ?? [],
'usersUpdate' => $stats['users.$all.requests.update'] ?? [],
'usersDelete' => $stats['users.$all.requests.delete'] ?? [],
'sessionsCreate' => $stats['sessions.$all.requests.create'] ?? [],
'sessionsProviderCreate' => $stats["sessions.$provider.requests.create"] ?? [],
'sessionsDelete' => $stats['sessions.$all.requests.delete' ?? []]
]);
}

View file

@ -7,7 +7,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Request;
use Utopia\App;
@ -145,13 +145,10 @@ App::init()
$usage
->setParam('projectId', $project->getId())
->setParam('httpRequest', 1)
->setParam('httpUrl', $request->getHostname() . $request->getURI())
->setParam('project.{scope}.network.requests', 1)
->setParam('httpMethod', $request->getMethod())
->setParam('httpPath', $route->getPath())
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->setParam('storage', 0);
->setParam('project.{scope}.network.inbound', 0)
->setParam('project.{scope}.network.outbound', 0);
$deletes->setProject($project);
$database->setProject($project);
@ -285,7 +282,7 @@ App::shutdown()
$bucket = $events->getContext('bucket');
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $project,
@ -402,10 +399,31 @@ App::shutdown()
&& $project->getId()
&& $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin
&& !empty($route->getLabel('sdk.namespace', null))
) {
) { // Don't calculate console usage on admin mode
$metric = $route->getLabel('usage.metric', '');
$usageParams = $route->getLabel('usage.params', '');
if (!empty($metric)) {
$usage->setParam($metric, 1);
foreach ($usageParams as $param) {
$param = $parseLabel($param, $responsePayload, $requestParams, $user);
$parts = explode(':', $param);
if (count($parts) != 2) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Usage params not properly set');
}
$usage->setParam($parts[0], $parts[1]);
}
}
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$usage
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize())
->submit();
->setParam('project.{scope}.network.inbound', $request->getSize() + $fileSize)
->setParam('project.{scope}.network.outbound', $response->getSize())
->submit();
}
});

View file

@ -40,7 +40,7 @@ use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Database\ID;

View file

@ -4,21 +4,25 @@ global $cli, $register;
use Appwrite\Stats\Usage;
use Appwrite\Stats\UsageDB;
use Appwrite\Usage\Calculators\Aggregator;
use Appwrite\Usage\Calculators\Database;
use Appwrite\Usage\Calculators\TimeSeries;
use InfluxDB\Database as InfluxDatabase;
use Utopia\App;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Database as UtopiaDatabase;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
use Utopia\Logger\Log;
use Utopia\Validator\WhiteList;
Authorization::disable();
Authorization::setDefaultStatus(false);
function getDatabase(Registry &$register, string $namespace): Database
function getDatabase(Registry &$register, string $namespace): UtopiaDatabase
{
$attempts = 0;
@ -30,7 +34,7 @@ function getDatabase(Registry &$register, string $namespace): Database
$redis = $register->get('cache');
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database = new UtopiaDatabase(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
@ -38,7 +42,7 @@ function getDatabase(Registry &$register, string $namespace): Database
throw new Exception('Projects collection not ready');
}
break; // leave loop if successful
} catch (\Exception$e) {
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
@ -65,7 +69,7 @@ function getInfluxDB(Registry &$register): InfluxDatabase
if (in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable$th) {
} catch (\Throwable $th) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
@ -110,55 +114,63 @@ $logError = function (Throwable $error, string $action = 'syncUsageStats') use (
Console::warning($error->getTraceAsString());
};
function aggregateTimeseries(UtopiaDatabase $database, InfluxDatabase $influxDB, callable $logError): void
{
$interval = (int) App::getEnv('_APP_USAGE_TIMESERIES_INTERVAL', '30'); // 30 seconds (by default)
$usage = new TimeSeries($database, $influxDB, $logError);
Console::loop(function () use ($interval, $usage) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating Timeseries Usage data every {$interval} seconds");
$loopStart = microtime(true);
$usage->collect();
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
}
function aggregateDatabase(UtopiaDatabase $database, callable $logError): void
{
$interval = (int) App::getEnv('_APP_USAGE_DATABASE_INTERVAL', '900'); // 15 minutes (by default)
$usage = new Database($database, $logError);
$aggregrator = new Aggregator($database, $logError);
Console::loop(function () use ($interval, $usage, $aggregrator) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating database usage every {$interval} seconds.");
$loopStart = microtime(true);
$usage->collect();
$aggregrator->collect();
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
}
$cli
->task('usage')
->param('type', 'timeseries', new WhiteList(['timeseries', 'database']))
->desc('Schedules syncing data from influxdb to Appwrite console db')
->action(function () use ($register, $logError) {
->action(function (string $type) use ($register, $logError) {
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)
$database = getDatabase($register, '_console');
$influxDB = getInfluxDB($register);
$usage = new Usage($database, $influxDB, $logError);
$usageDB = new UsageDB($database, $logError);
$iterations = 0;
Console::loop(function () use ($interval, $usage, $usageDB, &$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
*/
$usage->collect();
if ($iterations % 30 != 0) { // return if 30 iterations has not passed
$iterations++;
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
return;
}
$iterations = 0; // Reset iterations to prevent overflow when running for long time
/**
* Aggregate MariaDB every 15 minutes
* Some of the queries here might contain full-table scans.
*/
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating database counters.");
$usageDB->collect();
$iterations++;
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
switch ($type) {
case 'timeseries':
aggregateTimeseries($database, $influxDB, $logError);
break;
case 'database':
aggregateDatabase($database, $logError);
break;
default:
Console::error("Unsupported usage aggregation type");
}
});

View file

@ -291,7 +291,7 @@ sort($patterns);
<li data-state="/console/functions/function/monitors?id={{router.params.id}}&project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-function-id="{{router.params.id}}"
@ -302,9 +302,10 @@ sort($patterns);
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-range="30d"
data-param-function-id="{{router.params.id}}">
<button class="tick">30d</button>
</form>
@ -312,7 +313,7 @@ sort($patterns);
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-function-id="{{router.params.id}}"
@ -325,44 +326,44 @@ sort($patterns);
<h2>Monitors</h2>
<div
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="load"
data-name="usage"
data-param-function-id="{{router.params.id}}">
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=functionsExecutions" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=executionsTotal" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Executions <span data-ls-bind="({{usage.functionsExecutions|statsGetLast|statsTotal}})"></span></li>
<li>Executions <span data-ls-bind="({{usage.executionsTotal|statsGetLast|statsTotal}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=functionsCompute" data-colors="orange" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=executionsTime" data-colors="orange" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="orange">CPU Time <span data-ls-bind="({{usage.functionsCompute|statsGetLast|seconds2hum}})"></span></li>
<li class="orange">CPU Time <span data-ls-bind="({{usage.executionsTime|statsGetLast|seconds2hum}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=functionsFailures" data-colors="red" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=executionsFailure" data-colors="red" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="red">Errors <span data-ls-bind="({{usage.functionsFailures|statsGetLast|statsTotal}})"></span></li>
<li class="red">Errors <span data-ls-bind="({{usage.executionsFailure|statsGetLast|statsTotal}})"></span></li>
</ul>
</div>
</li>

View file

@ -1,5 +1,6 @@
<?php
$runtimes = $this->getParam('runtimes', []);
$usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
?>
<div class="cover">
<h1 class="zone xl margin-bottom-large">
@ -136,5 +137,82 @@ $runtimes = $this->getParam('runtimes', []);
</div>
</div>
</li>
<?php if ($usageStatsEnabled): ?>
<li data-state="/console/functions/usage?id={{router.params.id}}&project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="90d">
<button class="tick">90d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="30d">
<button class="tick">30d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="24h">
<button class="tick">24h</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
<h2>Usage</h2>
<div
data-service="functions.getUsage"
data-event="load"
data-name="usage">
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=executionsTotal" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Executions <span data-ls-bind="({{usage.executionsTotal|statsGetLast|statsTotal}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=executionsTime" data-colors="orange" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="orange">CPU Time <span data-ls-bind="({{usage.executionsTime|statsGetLast|seconds2hum}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=executionsFailure" data-colors="red" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="red">Errors <span data-ls-bind="({{usage.executionsFailure|statsGetLast|statsTotal}})"></span></li>
</ul>
</div>
</li>
<?php endif;?>
</ul>
</div>

View file

@ -550,10 +550,12 @@ services:
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
appwrite-usage-timeseries:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: usage
container_name: appwrite-usage
entrypoint:
- usage
- --type=timeseries
container_name: appwrite-usage-timeseries
<<: *x-logging
restart: unless-stopped
networks:
@ -571,7 +573,40 @@ services:
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-usage-database:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint:
- usage
- --type=database
container_name: appwrite-usage-database
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER

View file

@ -6,6 +6,7 @@ use Appwrite\Resque\Worker;
use Appwrite\Utopia\Response\Model\Deployment;
use Cron\CronExpression;
use Executor\Executor;
use Appwrite\Usage\Stats;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\App;
@ -221,6 +222,22 @@ class BuildsV1 extends Worker
channels: $target['channels'],
roles: $target['roles']
);
/** Update usage stats */
global $register;
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
$statsd = $register->get('statsd');
$usage = new Stats($statsd);
$usage
->setParam('projectId', $project->getId())
->setParam('functionId', $function->getId())
->setParam('builds.{scope}.compute', 1)
->setParam('buildStatus', $build->getAttribute('status', ''))
->setParam('buildTime', $build->getAttribute('duration'))
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();
}
}
}

View file

@ -4,7 +4,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\Response\Model\Execution;
use Cron\CronExpression;
use Executor\Executor;
@ -368,9 +368,9 @@ class FunctionsV1 extends Worker
$usage
->setParam('projectId', $project->getId())
->setParam('functionId', $function->getId())
->setParam('functionExecution', 1)
->setParam('functionStatus', $execution->getAttribute('status', ''))
->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000) // ms
->setParam('executions.{scope}.compute', 1)
->setParam('executionStatus', $execution->getAttribute('status', ''))
->setParam('executionTime', $execution->getAttribute('time'))
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();

View file

@ -45,13 +45,13 @@
"appwrite/php-runtimes": "0.11.*",
"utopia-php/framework": "0.21.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.10.*",
"utopia-php/abuse": "0.11.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.11.*",
"utopia-php/audit": "0.12.*",
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "dev-refactor-permissions as 0.22.0",
"utopia-php/database": "dev-refactor-permissions as 0.23.0",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",

34
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": "0d6daae2c9fa0cb94684996f3121b6be",
"content-hash": "64351ec59c6d50023ef9f6195777709b",
"packages": [
{
"name": "adhocore/jwt",
@ -1733,23 +1733,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.10.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0"
"reference": "f1096b92a8c47b19b0c55096775c186cab0b0a97"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/f1096b92a8c47b19b0c55096775c186cab0b0a97",
"reference": "f1096b92a8c47b19b0c55096775c186cab0b0a97",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.22.0"
"utopia-php/database": "0.23.0"
},
"require-dev": {
"phpunit/phpunit": "^9.4",
@ -1781,9 +1781,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.10.0"
"source": "https://github.com/utopia-php/abuse/tree/0.11.0"
},
"time": "2022-08-17T14:31:54+00:00"
"time": "2022-08-19T08:47:17+00:00"
},
{
"name": "utopia-php/analytics",
@ -1842,22 +1842,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.11.0",
"version": "0.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250"
"reference": "fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168",
"reference": "fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.22.0"
"utopia-php/database": "0.23.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -1889,9 +1889,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.11.0"
"source": "https://github.com/utopia-php/audit/tree/0.12.0"
},
"time": "2022-08-17T15:08:58+00:00"
"time": "2022-08-19T08:47:16+00:00"
},
{
"name": "utopia-php/cache",
@ -5358,8 +5358,8 @@
{
"package": "utopia-php/database",
"version": "dev-refactor-permissions",
"alias": "0.22.0",
"alias_normalized": "0.22.0.0"
"alias": "0.23.0",
"alias_normalized": "0.23.0.0"
}
],
"minimum-stability": "stable",

View file

@ -582,10 +582,12 @@ services:
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
entrypoint: usage
appwrite-usage-timeseries:
entrypoint:
- usage
- --type=timeseries
<<: *x-logging
container_name: appwrite-usage
container_name: appwrite-usage-timeseries
build:
context: .
args:
@ -609,7 +611,46 @@ services:
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-usage-database:
entrypoint:
- usage
- --type=database
<<: *x-logging
container_name: appwrite-usage-database
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_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER

View file

@ -0,0 +1 @@
Get the database activity logs list by its unique ID.

File diff suppressed because one or more lines are too long

View file

@ -66,9 +66,9 @@ updatePassword(password,oldPassword){return __awaiter(this,void 0,void 0,functio
let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;}
if(typeof oldPassword!=='undefined'){payload['oldPassword']=oldPassword;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('patch',uri,{'content-type':'application/json',},payload);});}
updatePhone(number,password){return __awaiter(this,void 0,void 0,function*(){if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
updatePhone(phone,password){return __awaiter(this,void 0,void 0,function*(){if(typeof phone==='undefined'){throw new AppwriteException('Missing required parameter: "phone"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account/phone';let payload={};if(typeof number!=='undefined'){payload['number']=number;}
let path='/account/phone';let payload={};if(typeof phone!=='undefined'){payload['phone']=phone;}
if(typeof password!=='undefined'){payload['password']=password;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('patch',uri,{'content-type':'application/json',},payload);});}
getPrefs(){return __awaiter(this,void 0,void 0,function*(){let path='/account/prefs';let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
@ -115,10 +115,10 @@ if(typeof scopes!=='undefined'){payload['scopes']=scopes;}
const uri=new URL(this.client.config.endpoint+path);payload['project']=this.client.config.project;for(const[key,value]of Object.entries(Service.flatten(payload))){uri.searchParams.append(key,value);}
if(typeof window!=='undefined'&&(window===null||window===void 0?void 0:window.location)){window.location.href=uri.toString();}
else{return uri;}}
createPhoneSession(userId,number){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
createPhoneSession(userId,phone){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof phone==='undefined'){throw new AppwriteException('Missing required parameter: "phone"');}
let path='/account/sessions/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof number!=='undefined'){payload['number']=number;}
if(typeof phone!=='undefined'){payload['phone']=phone;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
updatePhoneSession(userId,secret){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
@ -441,6 +441,8 @@ if(typeof schedule!=='undefined'){payload['schedule']=schedule;}
if(typeof timeout!=='undefined'){payload['timeout']=timeout;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
listRuntimes(){return __awaiter(this,void 0,void 0,function*(){let path='/functions/runtimes';let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
getUsage(range){return __awaiter(this,void 0,void 0,function*(){let path='/functions/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
get(functionId){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
update(functionId,name,execute,events,schedule,timeout){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
@ -502,7 +504,7 @@ const uri=new URL(this.client.config.endpoint+path);return yield this.client.cal
getExecution(functionId,executionId){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
if(typeof executionId==='undefined'){throw new AppwriteException('Missing required parameter: "executionId"');}
let path='/functions/{functionId}/executions/{executionId}'.replace('{functionId}',functionId).replace('{executionId}',executionId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
getUsage(functionId,range){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
getFunctionUsage(functionId,range){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
let path='/functions/{functionId}/usage'.replace('{functionId}',functionId);let payload={};if(typeof range!=='undefined'){payload['range']=range;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
listVariables(functionId){return __awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
@ -875,11 +877,10 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
create(userId,email,password,name){return __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"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
create(userId,email,phone,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof phone!=='undefined'){payload['phone']=phone;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
@ -1010,12 +1011,91 @@ updatePhoneVerification(userId,phoneVerification){return __awaiter(this,void 0,v
if(typeof phoneVerification==='undefined'){throw new AppwriteException('Missing required parameter: "phoneVerification"');}
let path='/users/{userId}/verification/phone'.replace('{userId}',userId);let payload={};if(typeof phoneVerification!=='undefined'){payload['phoneVerification']=phoneVerification;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('patch',uri,{'content-type':'application/json',},payload);});}}
exports.Account=Account;exports.AppwriteException=AppwriteException;exports.Avatars=Avatars;exports.Client=Client;exports.Databases=Databases;exports.Functions=Functions;exports.Health=Health;exports.Locale=Locale;exports.Projects=Projects;exports.Query=Query;exports.Storage=Storage;exports.Teams=Teams;exports.Users=Users;Object.defineProperty(exports,'__esModule',{value:true});})(this.Appwrite=this.Appwrite||{},null,window);(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?module.exports=factory():typeof define==='function'&&define.amd?define(factory):(global=typeof globalThis!=='undefined'?globalThis:global||self,global.Chart=factory());})(this,(function(){'use strict';function fontString(pixelSize,fontStyle,fontFamily){return fontStyle+' '+pixelSize+'px '+fontFamily;}
exports.Account=Account;exports.AppwriteException=AppwriteException;exports.Avatars=Avatars;exports.Client=Client;exports.Databases=Databases;exports.Functions=Functions;exports.Health=Health;exports.Locale=Locale;exports.Projects=Projects;exports.Query=Query;exports.Storage=Storage;exports.Teams=Teams;exports.Users=Users;Object.defineProperty(exports,'__esModule',{value:true});})(this.Appwrite=this.Appwrite||{},null,window);(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?module.exports=factory():typeof define==='function'&&define.amd?define(factory):(global=typeof globalThis!=='undefined'?globalThis:global||self,global.Chart=factory());})(this,(function(){'use strict';function noop(){}
const uid=(function(){let id=0;return function(){return id++;};}());function isNullOrUndef(value){return value===null||typeof value==='undefined';}
function isArray(value){if(Array.isArray&&Array.isArray(value)){return true;}
const type=Object.prototype.toString.call(value);if(type.slice(0,7)==='[object'&&type.slice(-6)==='Array]'){return true;}
return false;}
function isObject(value){return value!==null&&Object.prototype.toString.call(value)==='[object Object]';}
const isNumberFinite=(value)=>(typeof value==='number'||value instanceof Number)&&isFinite(+value);function finiteOrDefault(value,defaultValue){return isNumberFinite(value)?value:defaultValue;}
function valueOrDefault(value,defaultValue){return typeof value==='undefined'?defaultValue:value;}
const toPercentage=(value,dimension)=>typeof value==='string'&&value.endsWith('%')?parseFloat(value)/100:value/dimension;const toDimension=(value,dimension)=>typeof value==='string'&&value.endsWith('%')?parseFloat(value)/100*dimension:+value;function callback(fn,args,thisArg){if(fn&&typeof fn.call==='function'){return fn.apply(thisArg,args);}}
function each(loopable,fn,thisArg,reverse){let i,len,keys;if(isArray(loopable)){len=loopable.length;if(reverse){for(i=len-1;i>=0;i--){fn.call(thisArg,loopable[i],i);}}else{for(i=0;i<len;i++){fn.call(thisArg,loopable[i],i);}}}else if(isObject(loopable)){keys=Object.keys(loopable);len=keys.length;for(i=0;i<len;i++){fn.call(thisArg,loopable[keys[i]],keys[i]);}}}
function _elementsEqual(a0,a1){let i,ilen,v0,v1;if(!a0||!a1||a0.length!==a1.length){return false;}
for(i=0,ilen=a0.length;i<ilen;++i){v0=a0[i];v1=a1[i];if(v0.datasetIndex!==v1.datasetIndex||v0.index!==v1.index){return false;}}
return true;}
function clone$1(source){if(isArray(source)){return source.map(clone$1);}
if(isObject(source)){const target=Object.create(null);const keys=Object.keys(source);const klen=keys.length;let k=0;for(;k<klen;++k){target[keys[k]]=clone$1(source[keys[k]]);}
return target;}
return source;}
function isValidKey(key){return['__proto__','prototype','constructor'].indexOf(key)===-1;}
function _merger(key,target,source,options){if(!isValidKey(key)){return;}
const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(sval)){merge(tval,sval,options);}else{target[key]=clone$1(sval);}}
function merge(target,source,options){const sources=isArray(source)?source:[source];const ilen=sources.length;if(!isObject(target)){return target;}
options=options||{};const merger=options.merger||_merger;for(let i=0;i<ilen;++i){source=sources[i];if(!isObject(source)){continue;}
const keys=Object.keys(source);for(let k=0,klen=keys.length;k<klen;++k){merger(keys[k],target,source,options);}}
return target;}
function mergeIf(target,source){return merge(target,source,{merger:_mergerIf});}
function _mergerIf(key,target,source){if(!isValidKey(key)){return;}
const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(sval)){mergeIf(tval,sval);}else if(!Object.prototype.hasOwnProperty.call(target,key)){target[key]=clone$1(sval);}}
function _deprecated(scope,value,previous,current){if(value!==undefined){console.warn(scope+': "'+previous+'" is deprecated. Please use "'+current+'" instead');}}
const keyResolvers={'':v=>v,x:o=>o.x,y:o=>o.y};function resolveObjectKey(obj,key){const resolver=keyResolvers[key]||(keyResolvers[key]=_getKeyResolver(key));return resolver(obj);}
function _getKeyResolver(key){const keys=_splitKey(key);return obj=>{for(const k of keys){if(k===''){break;}
obj=obj&&obj[k];}
return obj;};}
function _splitKey(key){const parts=key.split('.');const keys=[];let tmp='';for(const part of parts){tmp+=part;if(tmp.endsWith('\\')){tmp=tmp.slice(0,-1)+'.';}else{keys.push(tmp);tmp='';}}
return keys;}
function _capitalize(str){return str.charAt(0).toUpperCase()+str.slice(1);}
const defined=(value)=>typeof value!=='undefined';const isFunction=(value)=>typeof value==='function';const setsEqual=(a,b)=>{if(a.size!==b.size){return false;}
for(const item of a){if(!b.has(item)){return false;}}
return true;};function _isClickEvent(e){return e.type==='mouseup'||e.type==='click'||e.type==='contextmenu';}
const PI=Math.PI;const TAU=2*PI;const PITAU=TAU+PI;const INFINITY=Number.POSITIVE_INFINITY;const RAD_PER_DEG=PI/180;const HALF_PI=PI/2;const QUARTER_PI=PI/4;const TWO_THIRDS_PI=PI*2/3;const log10=Math.log10;const sign=Math.sign;function niceNum(range){const roundedRange=Math.round(range);range=almostEquals(range,roundedRange,range/1000)?roundedRange:range;const niceRange=Math.pow(10,Math.floor(log10(range)));const fraction=range/niceRange;const niceFraction=fraction<=1?1:fraction<=2?2:fraction<=5?5:10;return niceFraction*niceRange;}
function _factorize(value){const result=[];const sqrt=Math.sqrt(value);let i;for(i=1;i<sqrt;i++){if(value%i===0){result.push(i);result.push(value/i);}}
if(sqrt===(sqrt|0)){result.push(sqrt);}
result.sort((a,b)=>a-b).pop();return result;}
function isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n);}
function almostEquals(x,y,epsilon){return Math.abs(x-y)<epsilon;}
function almostWhole(x,epsilon){const rounded=Math.round(x);return((rounded-epsilon)<=x)&&((rounded+epsilon)>=x);}
function _setMinAndMaxByKey(array,target,property){let i,ilen,value;for(i=0,ilen=array.length;i<ilen;i++){value=array[i][property];if(!isNaN(value)){target.min=Math.min(target.min,value);target.max=Math.max(target.max,value);}}}
function toRadians(degrees){return degrees*(PI/180);}
function toDegrees(radians){return radians*(180/PI);}
function _decimalPlaces(x){if(!isNumberFinite(x)){return;}
let e=1;let p=0;while(Math.round(x*e)/e!==x){e*=10;p++;}
return p;}
function getAngleFromPoint(centrePoint,anglePoint){const distanceFromXCenter=anglePoint.x-centrePoint.x;const distanceFromYCenter=anglePoint.y-centrePoint.y;const radialDistanceFromCenter=Math.sqrt(distanceFromXCenter*distanceFromXCenter+distanceFromYCenter*distanceFromYCenter);let angle=Math.atan2(distanceFromYCenter,distanceFromXCenter);if(angle<(-0.5*PI)){angle+=TAU;}
return{angle,distance:radialDistanceFromCenter};}
function distanceBetweenPoints(pt1,pt2){return Math.sqrt(Math.pow(pt2.x-pt1.x,2)+Math.pow(pt2.y-pt1.y,2));}
function _angleDiff(a,b){return(a-b+PITAU)%TAU-PI;}
function _normalizeAngle(a){return(a%TAU+TAU)%TAU;}
function _angleBetween(angle,start,end,sameAngleIsFullCircle){const a=_normalizeAngle(angle);const s=_normalizeAngle(start);const e=_normalizeAngle(end);const angleToStart=_normalizeAngle(s-a);const angleToEnd=_normalizeAngle(e-a);const startToAngle=_normalizeAngle(a-s);const endToAngle=_normalizeAngle(a-e);return a===s||a===e||(sameAngleIsFullCircle&&s===e)||(angleToStart>angleToEnd&&startToAngle<endToAngle);}
function _limitValue(value,min,max){return Math.max(min,Math.min(max,value));}
function _int16Range(value){return _limitValue(value,-32768,32767);}
function _isBetween(value,start,end,epsilon=1e-6){return value>=Math.min(start,end)-epsilon&&value<=Math.max(start,end)+epsilon;}
function _lookup(table,value,cmp){cmp=cmp||((index)=>table[index]<value);let hi=table.length-1;let lo=0;let mid;while(hi-lo>1){mid=(lo+hi)>>1;if(cmp(mid)){lo=mid;}else{hi=mid;}}
return{lo,hi};}
const _lookupByKey=(table,key,value,last)=>_lookup(table,value,last?index=>table[index][key]<=value:index=>table[index][key]<value);const _rlookupByKey=(table,key,value)=>_lookup(table,value,index=>table[index][key]>=value);function _filterBetween(values,min,max){let start=0;let end=values.length;while(start<end&&values[start]<min){start++;}
while(end>start&&values[end-1]>max){end--;}
return start>0||end<values.length?values.slice(start,end):values;}
const arrayEvents=['push','pop','shift','splice','unshift'];function listenArrayEvents(array,listener){if(array._chartjs){array._chartjs.listeners.push(listener);return;}
Object.defineProperty(array,'_chartjs',{configurable:true,enumerable:false,value:{listeners:[listener]}});arrayEvents.forEach((key)=>{const method='_onData'+_capitalize(key);const base=array[key];Object.defineProperty(array,key,{configurable:true,enumerable:false,value(...args){const res=base.apply(this,args);array._chartjs.listeners.forEach((object)=>{if(typeof object[method]==='function'){object[method](...args);}});return res;}});});}
function unlistenArrayEvents(array,listener){const stub=array._chartjs;if(!stub){return;}
const listeners=stub.listeners;const index=listeners.indexOf(listener);if(index!==-1){listeners.splice(index,1);}
if(listeners.length>0){return;}
arrayEvents.forEach((key)=>{delete array[key];});delete array._chartjs;}
function _arrayUnique(items){const set=new Set();let i,ilen;for(i=0,ilen=items.length;i<ilen;++i){set.add(items[i]);}
if(set.size===ilen){return items;}
return Array.from(set);}
function fontString(pixelSize,fontStyle,fontFamily){return fontStyle+' '+pixelSize+'px '+fontFamily;}
const requestAnimFrame=(function(){if(typeof window==='undefined'){return function(callback){return callback();};}
return window.requestAnimationFrame;}());function throttled(fn,thisArg,updateFn){const updateArgs=updateFn||((args)=>Array.prototype.slice.call(args));let ticking=false;let args=[];return function(...rest){args=updateArgs(rest);if(!ticking){ticking=true;requestAnimFrame.call(window,()=>{ticking=false;fn.apply(thisArg,args);});}};}
function debounce(fn,delay){let timeout;return function(...args){if(delay){clearTimeout(timeout);timeout=setTimeout(fn,delay,args);}else{fn.apply(this,args);}
return delay;};}
const _toLeftRightCenter=(align)=>align==='start'?'left':align==='end'?'right':'center';const _alignStartEnd=(align,start,end)=>align==='start'?start:align==='end'?end:(start+end)/2;const _textX=(align,left,right,rtl)=>{const check=rtl?'left':'right';return align===check?right:align==='center'?(left+right)/2:left;};class Animator{constructor(){this._request=null;this._charts=new Map();this._running=false;this._lastDate=undefined;}
const _toLeftRightCenter=(align)=>align==='start'?'left':align==='end'?'right':'center';const _alignStartEnd=(align,start,end)=>align==='start'?start:align==='end'?end:(start+end)/2;const _textX=(align,left,right,rtl)=>{const check=rtl?'left':'right';return align===check?right:align==='center'?(left+right)/2:left;};function _getStartAndCountOfVisiblePoints(meta,points,animationsDisabled){const pointCount=points.length;let start=0;let count=pointCount;if(meta._sorted){const{iScale,_parsed}=meta;const axis=iScale.axis;const{min,max,minDefined,maxDefined}=iScale.getUserBounds();if(minDefined){start=_limitValue(Math.min(_lookupByKey(_parsed,iScale.axis,min).lo,animationsDisabled?pointCount:_lookupByKey(points,axis,iScale.getPixelForValue(min)).lo),0,pointCount-1);}
if(maxDefined){count=_limitValue(Math.max(_lookupByKey(_parsed,iScale.axis,max,true).hi+1,animationsDisabled?0:_lookupByKey(points,axis,iScale.getPixelForValue(max),true).hi+1),start,pointCount)-start;}else{count=pointCount-start;}}
return{start,count};}
function _scaleRangesChanged(meta){const{xScale,yScale,_scaleRanges}=meta;const newRanges={xmin:xScale.min,xmax:xScale.max,ymin:yScale.min,ymax:yScale.max};if(!_scaleRanges){meta._scaleRanges=newRanges;return true;}
const changed=_scaleRanges.xmin!==xScale.min||_scaleRanges.xmax!==xScale.max||_scaleRanges.ymin!==yScale.min||_scaleRanges.ymax!==yScale.max;Object.assign(_scaleRanges,newRanges);return changed;}
class Animator{constructor(){this._request=null;this._charts=new Map();this._running=false;this._lastDate=undefined;}
_notify(chart,anims,date,type){const callbacks=anims.listeners[type];const numSteps=anims.duration;callbacks.forEach(fn=>fn({chart,initial:anims.initial,numSteps,currentStep:Math.min(date-anims.start,numSteps)}));}
_refresh(){if(this._request){return;}
this._running=true;this._request=requestAnimFrame.call(window,()=>{this._update();this._request=null;if(this._running){this._refresh();}});}
@ -1081,8 +1161,8 @@ r=+m[1];g=+m[3];b=+m[5];r=255&(m[2]?p2b(r):lim(r,0,255));g=255&(m[4]?p2b(g):lim(
function rgbString(v){return v&&(v.a<255?`rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})`:`rgb(${v.r}, ${v.g}, ${v.b})`);}
const to=v=>v<=0.0031308?v*12.92:Math.pow(v,1.0/2.4)*1.055-0.055;const from=v=>v<=0.04045?v/12.92:Math.pow((v+0.055)/1.055,2.4);function interpolate$1(rgb1,rgb2,t){const r=from(b2n(rgb1.r));const g=from(b2n(rgb1.g));const b=from(b2n(rgb1.b));return{r:n2b(to(r+t*(from(b2n(rgb2.r))-r))),g:n2b(to(g+t*(from(b2n(rgb2.g))-g))),b:n2b(to(b+t*(from(b2n(rgb2.b))-b))),a:rgb1.a+t*(rgb2.a-rgb1.a)};}
function modHSL(v,i,ratio){if(v){let tmp=rgb2hsl(v);tmp[i]=Math.max(0,Math.min(tmp[i]+tmp[i]*ratio,i===0?360:1));tmp=hsl2rgb(tmp);v.r=tmp[0];v.g=tmp[1];v.b=tmp[2];}}
function clone$1(v,proto){return v?Object.assign(proto||{},v):v;}
function fromObject(input){var v={r:0,g:0,b:0,a:255};if(Array.isArray(input)){if(input.length>=3){v={r:input[0],g:input[1],b:input[2],a:255};if(input.length>3){v.a=n2b(input[3]);}}}else{v=clone$1(input,{r:0,g:0,b:0,a:1});v.a=n2b(v.a);}
function clone(v,proto){return v?Object.assign(proto||{},v):v;}
function fromObject(input){var v={r:0,g:0,b:0,a:255};if(Array.isArray(input)){if(input.length>=3){v={r:input[0],g:input[1],b:input[2],a:255};if(input.length>3){v.a=n2b(input[3]);}}}else{v=clone(input,{r:0,g:0,b:0,a:1});v.a=n2b(v.a);}
return v;}
function functionParse(str){if(str.charAt(0)==='r'){return rgbParse(str);}
return hueParse(str);}
@ -1090,7 +1170,7 @@ class Color{constructor(input){if(input instanceof Color){return input;}
const type=typeof input;let v;if(type==='object'){v=fromObject(input);}else if(type==='string'){v=hexParse(input)||nameParse(input)||functionParse(input);}
this._rgb=v;this._valid=!!v;}
get valid(){return this._valid;}
get rgb(){var v=clone$1(this._rgb);if(v){v.a=b2n(v.a);}
get rgb(){var v=clone(this._rgb);if(v){v.a=b2n(v.a);}
return v;}
set rgb(obj){this._rgb=fromObject(obj);}
rgbString(){return this._valid?rgbString(this._rgb):undefined;}
@ -1116,42 +1196,6 @@ function isPatternOrGradient(value){if(value&&typeof value==='object'){const typ
return false;}
function color(value){return isPatternOrGradient(value)?value:index_esm(value);}
function getHoverColor(value){return isPatternOrGradient(value)?value:index_esm(value).saturate(0.5).darken(0.1).hexString();}
function noop(){}
const uid=(function(){let id=0;return function(){return id++;};}());function isNullOrUndef(value){return value===null||typeof value==='undefined';}
function isArray(value){if(Array.isArray&&Array.isArray(value)){return true;}
const type=Object.prototype.toString.call(value);if(type.slice(0,7)==='[object'&&type.slice(-6)==='Array]'){return true;}
return false;}
function isObject(value){return value!==null&&Object.prototype.toString.call(value)==='[object Object]';}
const isNumberFinite=(value)=>(typeof value==='number'||value instanceof Number)&&isFinite(+value);function finiteOrDefault(value,defaultValue){return isNumberFinite(value)?value:defaultValue;}
function valueOrDefault(value,defaultValue){return typeof value==='undefined'?defaultValue:value;}
const toPercentage=(value,dimension)=>typeof value==='string'&&value.endsWith('%')?parseFloat(value)/100:value/dimension;const toDimension=(value,dimension)=>typeof value==='string'&&value.endsWith('%')?parseFloat(value)/100*dimension:+value;function callback(fn,args,thisArg){if(fn&&typeof fn.call==='function'){return fn.apply(thisArg,args);}}
function each(loopable,fn,thisArg,reverse){let i,len,keys;if(isArray(loopable)){len=loopable.length;if(reverse){for(i=len-1;i>=0;i--){fn.call(thisArg,loopable[i],i);}}else{for(i=0;i<len;i++){fn.call(thisArg,loopable[i],i);}}}else if(isObject(loopable)){keys=Object.keys(loopable);len=keys.length;for(i=0;i<len;i++){fn.call(thisArg,loopable[keys[i]],keys[i]);}}}
function _elementsEqual(a0,a1){let i,ilen,v0,v1;if(!a0||!a1||a0.length!==a1.length){return false;}
for(i=0,ilen=a0.length;i<ilen;++i){v0=a0[i];v1=a1[i];if(v0.datasetIndex!==v1.datasetIndex||v0.index!==v1.index){return false;}}
return true;}
function clone(source){if(isArray(source)){return source.map(clone);}
if(isObject(source)){const target=Object.create(null);const keys=Object.keys(source);const klen=keys.length;let k=0;for(;k<klen;++k){target[keys[k]]=clone(source[keys[k]]);}
return target;}
return source;}
function isValidKey(key){return['__proto__','prototype','constructor'].indexOf(key)===-1;}
function _merger(key,target,source,options){if(!isValidKey(key)){return;}
const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(sval)){merge(tval,sval,options);}else{target[key]=clone(sval);}}
function merge(target,source,options){const sources=isArray(source)?source:[source];const ilen=sources.length;if(!isObject(target)){return target;}
options=options||{};const merger=options.merger||_merger;for(let i=0;i<ilen;++i){source=sources[i];if(!isObject(source)){continue;}
const keys=Object.keys(source);for(let k=0,klen=keys.length;k<klen;++k){merger(keys[k],target,source,options);}}
return target;}
function mergeIf(target,source){return merge(target,source,{merger:_mergerIf});}
function _mergerIf(key,target,source){if(!isValidKey(key)){return;}
const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(sval)){mergeIf(tval,sval);}else if(!Object.prototype.hasOwnProperty.call(target,key)){target[key]=clone(sval);}}
function _deprecated(scope,value,previous,current){if(value!==undefined){console.warn(scope+': "'+previous+'" is deprecated. Please use "'+current+'" instead');}}
const emptyString='';const dot='.';function indexOfDotOrLength(key,start){const idx=key.indexOf(dot,start);return idx===-1?key.length:idx;}
function resolveObjectKey(obj,key){if(key===emptyString){return obj;}
let pos=0;let idx=indexOfDotOrLength(key,pos);while(obj&&idx>pos){obj=obj[key.slice(pos,idx)];pos=idx+1;idx=indexOfDotOrLength(key,pos);}
return obj;}
function _capitalize(str){return str.charAt(0).toUpperCase()+str.slice(1);}
const defined=(value)=>typeof value!=='undefined';const isFunction=(value)=>typeof value==='function';const setsEqual=(a,b)=>{if(a.size!==b.size){return false;}
for(const item of a){if(!b.has(item)){return false;}}
return true;};function _isClickEvent(e){return e.type==='mouseup'||e.type==='click'||e.type==='contextmenu';}
const overrides=Object.create(null);const descriptors=Object.create(null);function getScope$1(node,key){if(!key){return node;}
const keys=key.split('.');for(let i=0,n=keys.length;i<n;++i){const k=keys[i];node=node[k]||(node[k]=Object.create(null));}
return node;}
@ -1164,43 +1208,7 @@ describe(scope,values){return set(descriptors,scope,values);}
override(scope,values){return set(overrides,scope,values);}
route(scope,name,targetScope,targetName){const scopeObject=getScope$1(this,scope);const targetScopeObject=getScope$1(this,targetScope);const privateName='_'+name;Object.defineProperties(scopeObject,{[privateName]:{value:scopeObject[name],writable:true},[name]:{enumerable:true,get(){const local=this[privateName];const target=targetScopeObject[targetName];if(isObject(local)){return Object.assign({},target,local);}
return valueOrDefault(local,target);},set(value){this[privateName]=value;}}});}}
var defaults=new Defaults({_scriptable:(name)=>!name.startsWith('on'),_indexable:(name)=>name!=='events',hover:{_fallback:'interaction'},interaction:{_scriptable:false,_indexable:false,}});function _lookup(table,value,cmp){cmp=cmp||((index)=>table[index]<value);let hi=table.length-1;let lo=0;let mid;while(hi-lo>1){mid=(lo+hi)>>1;if(cmp(mid)){lo=mid;}else{hi=mid;}}
return{lo,hi};}
const _lookupByKey=(table,key,value)=>_lookup(table,value,index=>table[index][key]<value);const _rlookupByKey=(table,key,value)=>_lookup(table,value,index=>table[index][key]>=value);function _filterBetween(values,min,max){let start=0;let end=values.length;while(start<end&&values[start]<min){start++;}
while(end>start&&values[end-1]>max){end--;}
return start>0||end<values.length?values.slice(start,end):values;}
const arrayEvents=['push','pop','shift','splice','unshift'];function listenArrayEvents(array,listener){if(array._chartjs){array._chartjs.listeners.push(listener);return;}
Object.defineProperty(array,'_chartjs',{configurable:true,enumerable:false,value:{listeners:[listener]}});arrayEvents.forEach((key)=>{const method='_onData'+_capitalize(key);const base=array[key];Object.defineProperty(array,key,{configurable:true,enumerable:false,value(...args){const res=base.apply(this,args);array._chartjs.listeners.forEach((object)=>{if(typeof object[method]==='function'){object[method](...args);}});return res;}});});}
function unlistenArrayEvents(array,listener){const stub=array._chartjs;if(!stub){return;}
const listeners=stub.listeners;const index=listeners.indexOf(listener);if(index!==-1){listeners.splice(index,1);}
if(listeners.length>0){return;}
arrayEvents.forEach((key)=>{delete array[key];});delete array._chartjs;}
function _arrayUnique(items){const set=new Set();let i,ilen;for(i=0,ilen=items.length;i<ilen;++i){set.add(items[i]);}
if(set.size===ilen){return items;}
return Array.from(set);}
const PI=Math.PI;const TAU=2*PI;const PITAU=TAU+PI;const INFINITY=Number.POSITIVE_INFINITY;const RAD_PER_DEG=PI/180;const HALF_PI=PI/2;const QUARTER_PI=PI/4;const TWO_THIRDS_PI=PI*2/3;const log10=Math.log10;const sign=Math.sign;function niceNum(range){const roundedRange=Math.round(range);range=almostEquals(range,roundedRange,range/1000)?roundedRange:range;const niceRange=Math.pow(10,Math.floor(log10(range)));const fraction=range/niceRange;const niceFraction=fraction<=1?1:fraction<=2?2:fraction<=5?5:10;return niceFraction*niceRange;}
function _factorize(value){const result=[];const sqrt=Math.sqrt(value);let i;for(i=1;i<sqrt;i++){if(value%i===0){result.push(i);result.push(value/i);}}
if(sqrt===(sqrt|0)){result.push(sqrt);}
result.sort((a,b)=>a-b).pop();return result;}
function isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n);}
function almostEquals(x,y,epsilon){return Math.abs(x-y)<epsilon;}
function almostWhole(x,epsilon){const rounded=Math.round(x);return((rounded-epsilon)<=x)&&((rounded+epsilon)>=x);}
function _setMinAndMaxByKey(array,target,property){let i,ilen,value;for(i=0,ilen=array.length;i<ilen;i++){value=array[i][property];if(!isNaN(value)){target.min=Math.min(target.min,value);target.max=Math.max(target.max,value);}}}
function toRadians(degrees){return degrees*(PI/180);}
function toDegrees(radians){return radians*(180/PI);}
function _decimalPlaces(x){if(!isNumberFinite(x)){return;}
let e=1;let p=0;while(Math.round(x*e)/e!==x){e*=10;p++;}
return p;}
function getAngleFromPoint(centrePoint,anglePoint){const distanceFromXCenter=anglePoint.x-centrePoint.x;const distanceFromYCenter=anglePoint.y-centrePoint.y;const radialDistanceFromCenter=Math.sqrt(distanceFromXCenter*distanceFromXCenter+distanceFromYCenter*distanceFromYCenter);let angle=Math.atan2(distanceFromYCenter,distanceFromXCenter);if(angle<(-0.5*PI)){angle+=TAU;}
return{angle,distance:radialDistanceFromCenter};}
function distanceBetweenPoints(pt1,pt2){return Math.sqrt(Math.pow(pt2.x-pt1.x,2)+Math.pow(pt2.y-pt1.y,2));}
function _angleDiff(a,b){return(a-b+PITAU)%TAU-PI;}
function _normalizeAngle(a){return(a%TAU+TAU)%TAU;}
function _angleBetween(angle,start,end,sameAngleIsFullCircle){const a=_normalizeAngle(angle);const s=_normalizeAngle(start);const e=_normalizeAngle(end);const angleToStart=_normalizeAngle(s-a);const angleToEnd=_normalizeAngle(e-a);const startToAngle=_normalizeAngle(a-s);const endToAngle=_normalizeAngle(a-e);return a===s||a===e||(sameAngleIsFullCircle&&s===e)||(angleToStart>angleToEnd&&startToAngle<endToAngle);}
function _limitValue(value,min,max){return Math.max(min,Math.min(max,value));}
function _int16Range(value){return _limitValue(value,-32768,32767);}
function _isBetween(value,start,end,epsilon=1e-6){return value>=Math.min(start,end)-epsilon&&value<=Math.max(start,end)+epsilon;}
function _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
var defaults=new Defaults({_scriptable:(name)=>!name.startsWith('on'),_indexable:(name)=>name!=='events',hover:{_fallback:'interaction'},interaction:{_scriptable:false,_indexable:false,}});function _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
function _getParentNode(domNode){let parent=domNode.parentNode;if(parent&&parent.toString()==='[object ShadowRoot]'){parent=parent.host;}
return parent;}
function parseMaxStyle(styleValue,node,parentProperty){let valueInPixels;if(typeof styleValue==='string'){valueInPixels=parseInt(styleValue,10);if(styleValue.indexOf('%')!==-1){valueInPixels=valueInPixels/100*node.parentNode[parentProperty];}}else{valueInPixels=styleValue;}
@ -1393,7 +1401,7 @@ if(start<i-1){addStyle(start,i-1,segment.loop,prevStyle);}}
return result;}
function readStyle(options){return{backgroundColor:options.backgroundColor,borderCapStyle:options.borderCapStyle,borderDash:options.borderDash,borderDashOffset:options.borderDashOffset,borderJoinStyle:options.borderJoinStyle,borderWidth:options.borderWidth,borderColor:options.borderColor};}
function styleChanged(style,prevStyle){return prevStyle&&JSON.stringify(style)!==JSON.stringify(prevStyle);}
var helpers=Object.freeze({__proto__:null,easingEffects:effects,isPatternOrGradient:isPatternOrGradient,color:color,getHoverColor:getHoverColor,noop:noop,uid:uid,isNullOrUndef:isNullOrUndef,isArray:isArray,isObject:isObject,isFinite:isNumberFinite,finiteOrDefault:finiteOrDefault,valueOrDefault:valueOrDefault,toPercentage:toPercentage,toDimension:toDimension,callback:callback,each:each,_elementsEqual:_elementsEqual,clone:clone,_merger:_merger,merge:merge,mergeIf:mergeIf,_mergerIf:_mergerIf,_deprecated:_deprecated,resolveObjectKey:resolveObjectKey,_capitalize:_capitalize,defined:defined,isFunction:isFunction,setsEqual:setsEqual,_isClickEvent:_isClickEvent,toFontString:toFontString,_measureText:_measureText,_longestText:_longestText,_alignPixel:_alignPixel,clearCanvas:clearCanvas,drawPoint:drawPoint,drawPointLegend:drawPointLegend,_isPointInArea:_isPointInArea,clipArea:clipArea,unclipArea:unclipArea,_steppedLineTo:_steppedLineTo,_bezierCurveTo:_bezierCurveTo,renderText:renderText,addRoundedRectPath:addRoundedRectPath,_lookup:_lookup,_lookupByKey:_lookupByKey,_rlookupByKey:_rlookupByKey,_filterBetween:_filterBetween,listenArrayEvents:listenArrayEvents,unlistenArrayEvents:unlistenArrayEvents,_arrayUnique:_arrayUnique,_createResolver:_createResolver,_attachContext:_attachContext,_descriptors:_descriptors,_parseObjectDataRadialScale:_parseObjectDataRadialScale,splineCurve:splineCurve,splineCurveMonotone:splineCurveMonotone,_updateBezierControlPoints:_updateBezierControlPoints,_isDomSupported:_isDomSupported,_getParentNode:_getParentNode,getStyle:getStyle,getRelativePosition:getRelativePosition,getMaximumSize:getMaximumSize,retinaScale:retinaScale,supportsEventListenerOptions:supportsEventListenerOptions,readUsedSize:readUsedSize,fontString:fontString,requestAnimFrame:requestAnimFrame,throttled:throttled,debounce:debounce,_toLeftRightCenter:_toLeftRightCenter,_alignStartEnd:_alignStartEnd,_textX:_textX,_pointInLine:_pointInLine,_steppedInterpolation:_steppedInterpolation,_bezierInterpolation:_bezierInterpolation,formatNumber:formatNumber,toLineHeight:toLineHeight,_readValueToProps:_readValueToProps,toTRBL:toTRBL,toTRBLCorners:toTRBLCorners,toPadding:toPadding,toFont:toFont,resolve:resolve,_addGrace:_addGrace,createContext:createContext,PI:PI,TAU:TAU,PITAU:PITAU,INFINITY:INFINITY,RAD_PER_DEG:RAD_PER_DEG,HALF_PI:HALF_PI,QUARTER_PI:QUARTER_PI,TWO_THIRDS_PI:TWO_THIRDS_PI,log10:log10,sign:sign,niceNum:niceNum,_factorize:_factorize,isNumber:isNumber,almostEquals:almostEquals,almostWhole:almostWhole,_setMinAndMaxByKey:_setMinAndMaxByKey,toRadians:toRadians,toDegrees:toDegrees,_decimalPlaces:_decimalPlaces,getAngleFromPoint:getAngleFromPoint,distanceBetweenPoints:distanceBetweenPoints,_angleDiff:_angleDiff,_normalizeAngle:_normalizeAngle,_angleBetween:_angleBetween,_limitValue:_limitValue,_int16Range:_int16Range,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});function binarySearch(metaset,axis,value,intersect){const{controller,data,_sorted}=metaset;const iScale=controller._cachedMeta.iScale;if(iScale&&axis===iScale.axis&&axis!=='r'&&_sorted&&data.length){const lookupMethod=iScale._reversePixels?_rlookupByKey:_lookupByKey;if(!intersect){return lookupMethod(data,axis,value);}else if(controller._sharedOptions){const el=data[0];const range=typeof el.getRange==='function'&&el.getRange(axis);if(range){const start=lookupMethod(data,axis,value-range);const end=lookupMethod(data,axis,value+range);return{lo:start.lo,hi:end.hi};}}}
var helpers=Object.freeze({__proto__:null,easingEffects:effects,isPatternOrGradient:isPatternOrGradient,color:color,getHoverColor:getHoverColor,noop:noop,uid:uid,isNullOrUndef:isNullOrUndef,isArray:isArray,isObject:isObject,isFinite:isNumberFinite,finiteOrDefault:finiteOrDefault,valueOrDefault:valueOrDefault,toPercentage:toPercentage,toDimension:toDimension,callback:callback,each:each,_elementsEqual:_elementsEqual,clone:clone$1,_merger:_merger,merge:merge,mergeIf:mergeIf,_mergerIf:_mergerIf,_deprecated:_deprecated,resolveObjectKey:resolveObjectKey,_splitKey:_splitKey,_capitalize:_capitalize,defined:defined,isFunction:isFunction,setsEqual:setsEqual,_isClickEvent:_isClickEvent,toFontString:toFontString,_measureText:_measureText,_longestText:_longestText,_alignPixel:_alignPixel,clearCanvas:clearCanvas,drawPoint:drawPoint,drawPointLegend:drawPointLegend,_isPointInArea:_isPointInArea,clipArea:clipArea,unclipArea:unclipArea,_steppedLineTo:_steppedLineTo,_bezierCurveTo:_bezierCurveTo,renderText:renderText,addRoundedRectPath:addRoundedRectPath,_lookup:_lookup,_lookupByKey:_lookupByKey,_rlookupByKey:_rlookupByKey,_filterBetween:_filterBetween,listenArrayEvents:listenArrayEvents,unlistenArrayEvents:unlistenArrayEvents,_arrayUnique:_arrayUnique,_createResolver:_createResolver,_attachContext:_attachContext,_descriptors:_descriptors,_parseObjectDataRadialScale:_parseObjectDataRadialScale,splineCurve:splineCurve,splineCurveMonotone:splineCurveMonotone,_updateBezierControlPoints:_updateBezierControlPoints,_isDomSupported:_isDomSupported,_getParentNode:_getParentNode,getStyle:getStyle,getRelativePosition:getRelativePosition,getMaximumSize:getMaximumSize,retinaScale:retinaScale,supportsEventListenerOptions:supportsEventListenerOptions,readUsedSize:readUsedSize,fontString:fontString,requestAnimFrame:requestAnimFrame,throttled:throttled,debounce:debounce,_toLeftRightCenter:_toLeftRightCenter,_alignStartEnd:_alignStartEnd,_textX:_textX,_getStartAndCountOfVisiblePoints:_getStartAndCountOfVisiblePoints,_scaleRangesChanged:_scaleRangesChanged,_pointInLine:_pointInLine,_steppedInterpolation:_steppedInterpolation,_bezierInterpolation:_bezierInterpolation,formatNumber:formatNumber,toLineHeight:toLineHeight,_readValueToProps:_readValueToProps,toTRBL:toTRBL,toTRBLCorners:toTRBLCorners,toPadding:toPadding,toFont:toFont,resolve:resolve,_addGrace:_addGrace,createContext:createContext,PI:PI,TAU:TAU,PITAU:PITAU,INFINITY:INFINITY,RAD_PER_DEG:RAD_PER_DEG,HALF_PI:HALF_PI,QUARTER_PI:QUARTER_PI,TWO_THIRDS_PI:TWO_THIRDS_PI,log10:log10,sign:sign,niceNum:niceNum,_factorize:_factorize,isNumber:isNumber,almostEquals:almostEquals,almostWhole:almostWhole,_setMinAndMaxByKey:_setMinAndMaxByKey,toRadians:toRadians,toDegrees:toDegrees,_decimalPlaces:_decimalPlaces,getAngleFromPoint:getAngleFromPoint,distanceBetweenPoints:distanceBetweenPoints,_angleDiff:_angleDiff,_normalizeAngle:_normalizeAngle,_angleBetween:_angleBetween,_limitValue:_limitValue,_int16Range:_int16Range,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});function binarySearch(metaset,axis,value,intersect){const{controller,data,_sorted}=metaset;const iScale=controller._cachedMeta.iScale;if(iScale&&axis===iScale.axis&&axis!=='r'&&_sorted&&data.length){const lookupMethod=iScale._reversePixels?_rlookupByKey:_lookupByKey;if(!intersect){return lookupMethod(data,axis,value);}else if(controller._sharedOptions){const el=data[0];const range=typeof el.getRange==='function'&&el.getRange(axis);if(range){const start=lookupMethod(data,axis,value-range);const end=lookupMethod(data,axis,value+range);return{lo:start.lo,hi:end.hi};}}}
return{lo:0,hi:data.length-1};}
function evaluateInteractionItems(chart,axis,position,handler,intersect){const metasets=chart.getSortedVisibleDatasetMetas();const value=position[axis];for(let i=0,ilen=metasets.length;i<ilen;++i){const{index,data}=metasets[i];const{lo,hi}=binarySearch(metasets[i],axis,value,intersect);for(let j=lo;j<=hi;++j){const element=data[j];if(!element.skip){handler(element,index,j);}}}}
function getDistanceMetricForAxis(axis){const useX=axis.indexOf('x')!==-1;const useY=axis.indexOf('y')!==-1;return function(pt1,pt2){const deltaX=useX?Math.abs(pt1.x-pt2.x):0;const deltaY=useY?Math.abs(pt1.y-pt2.y):0;return Math.sqrt(Math.pow(deltaX,2)+Math.pow(deltaY,2));};}
@ -1764,7 +1772,7 @@ return this.getMatchingVisibleMetas().length>0;}
_computeGridLineItems(chartArea){const axis=this.axis;const chart=this.chart;const options=this.options;const{grid,position}=options;const offset=grid.offset;const isHorizontal=this.isHorizontal();const ticks=this.ticks;const ticksLength=ticks.length+(offset?1:0);const tl=getTickMarkLength(grid);const items=[];const borderOpts=grid.setContext(this.getContext());const axisWidth=borderOpts.drawBorder?borderOpts.borderWidth:0;const axisHalfWidth=axisWidth/2;const alignBorderValue=function(pixel){return _alignPixel(chart,pixel,axisWidth);};let borderValue,i,lineValue,alignedLineValue;let tx1,ty1,tx2,ty2,x1,y1,x2,y2;if(position==='top'){borderValue=alignBorderValue(this.bottom);ty1=this.bottom-tl;ty2=borderValue-axisHalfWidth;y1=alignBorderValue(chartArea.top)+axisHalfWidth;y2=chartArea.bottom;}else if(position==='bottom'){borderValue=alignBorderValue(this.top);y1=chartArea.top;y2=alignBorderValue(chartArea.bottom)-axisHalfWidth;ty1=borderValue+axisHalfWidth;ty2=this.top+tl;}else if(position==='left'){borderValue=alignBorderValue(this.right);tx1=this.right-tl;tx2=borderValue-axisHalfWidth;x1=alignBorderValue(chartArea.left)+axisHalfWidth;x2=chartArea.right;}else if(position==='right'){borderValue=alignBorderValue(this.left);x1=chartArea.left;x2=alignBorderValue(chartArea.right)-axisHalfWidth;tx1=borderValue+axisHalfWidth;tx2=this.left+tl;}else if(axis==='x'){if(position==='center'){borderValue=alignBorderValue((chartArea.top+chartArea.bottom)/2+0.5);}else if(isObject(position)){const positionAxisID=Object.keys(position)[0];const value=position[positionAxisID];borderValue=alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));}
y1=chartArea.top;y2=chartArea.bottom;ty1=borderValue+axisHalfWidth;ty2=ty1+tl;}else if(axis==='y'){if(position==='center'){borderValue=alignBorderValue((chartArea.left+chartArea.right)/2);}else if(isObject(position)){const positionAxisID=Object.keys(position)[0];const value=position[positionAxisID];borderValue=alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));}
tx1=borderValue-axisHalfWidth;tx2=tx1-tl;x1=chartArea.left;x2=chartArea.right;}
const limit=valueOrDefault(options.ticks.maxTicksLimit,ticksLength);const step=Math.max(1,Math.ceil(ticksLength/limit));for(i=0;i<ticksLength;i+=step){const optsAtIndex=grid.setContext(this.getContext(i));const lineWidth=optsAtIndex.lineWidth;const lineColor=optsAtIndex.color;const borderDash=grid.borderDash||[];const borderDashOffset=optsAtIndex.borderDashOffset;const tickWidth=optsAtIndex.tickWidth;const tickColor=optsAtIndex.tickColor;const tickBorderDash=optsAtIndex.tickBorderDash||[];const tickBorderDashOffset=optsAtIndex.tickBorderDashOffset;lineValue=getPixelForGridLine(this,i,offset);if(lineValue===undefined){continue;}
const limit=valueOrDefault(options.ticks.maxTicksLimit,ticksLength);const step=Math.max(1,Math.ceil(ticksLength/limit));for(i=0;i<ticksLength;i+=step){const optsAtIndex=grid.setContext(this.getContext(i));const lineWidth=optsAtIndex.lineWidth;const lineColor=optsAtIndex.color;const borderDash=optsAtIndex.borderDash||[];const borderDashOffset=optsAtIndex.borderDashOffset;const tickWidth=optsAtIndex.tickWidth;const tickColor=optsAtIndex.tickColor;const tickBorderDash=optsAtIndex.tickBorderDash||[];const tickBorderDashOffset=optsAtIndex.tickBorderDashOffset;lineValue=getPixelForGridLine(this,i,offset);if(lineValue===undefined){continue;}
alignedLineValue=_alignPixel(chart,lineValue,lineWidth);if(isHorizontal){tx1=tx2=x1=x2=alignedLineValue;}else{ty1=ty2=y1=y2=alignedLineValue;}
items.push({tx1,ty1,tx2,ty2,x1,y1,x2,y2,width:lineWidth,color:lineColor,borderDash,borderDashOffset,tickWidth,tickColor,tickBorderDash,tickBorderDashOffset,});}
this._ticksLength=ticksLength;this._borderValue=borderValue;return items;}
@ -1918,7 +1926,7 @@ const cacheKey=prefixes.join();let cached=cache.get(cacheKey);if(!cached){const
return cached;}
const hasFunction=value=>isObject(value)&&Object.getOwnPropertyNames(value).reduce((acc,key)=>acc||isFunction(value[key]),false);function needContext(proxy,names){const{isScriptable,isIndexable}=_descriptors(proxy);for(const prop of names){const scriptable=isScriptable(prop);const indexable=isIndexable(prop);const value=(indexable||scriptable)&&proxy[prop];if((scriptable&&(isFunction(value)||hasFunction(value)))||(indexable&&isArray(value))){return true;}}
return false;}
var version="3.8.2";const KNOWN_POSITIONS=['top','bottom','left','right','chartArea'];function positionIsHorizontal(position,axis){return position==='top'||position==='bottom'||(KNOWN_POSITIONS.indexOf(position)===-1&&axis==='x');}
var version="3.9.1";const KNOWN_POSITIONS=['top','bottom','left','right','chartArea'];function positionIsHorizontal(position,axis){return position==='top'||position==='bottom'||(KNOWN_POSITIONS.indexOf(position)===-1&&axis==='x');}
function compare2Level(l1,l2){return function(a,b){return a[l1]===b[l1]?a[l2]-b[l2]:a[l1]-b[l1];};}
function onAnimationsComplete(context){const chart=context.chart;const animationOptions=chart.options.animation;chart.notifyPlugins('afterRender');callback(animationOptions&&animationOptions.onComplete,[context],chart);}
function onAnimationProgress(context){const chart=context.chart;const animationOptions=chart.options.animation;callback(animationOptions&&animationOptions.onProgress,[context],chart);}
@ -2045,6 +2053,7 @@ if(!inChartArea){return lastActive;}
const hoverOptions=this.options.hover;return this.getElementsAtEventForMode(e,hoverOptions.mode,hoverOptions,useFinalPosition);}}
const invalidatePlugins=()=>each(Chart.instances,(chart)=>chart._plugins.invalidate());const enumerable=true;Object.defineProperties(Chart,{defaults:{enumerable,value:defaults},instances:{enumerable,value:instances},overrides:{enumerable,value:overrides},registry:{enumerable,value:registry},version:{enumerable,value:version},getChart:{enumerable,value:getChart},register:{enumerable,value:(...items)=>{registry.add(...items);invalidatePlugins();}},unregister:{enumerable,value:(...items)=>{registry.remove(...items);invalidatePlugins();}}});function abstract(){throw new Error('This method is not implemented: Check that a complete date adapter is provided.');}
class DateAdapter{constructor(options){this.options=options||{};}
init(chartOptions){}
formats(){return abstract();}
parse(value,format){return abstract();}
format(timestamp,format){return abstract();}
@ -2078,6 +2087,7 @@ function borderProps(properties){let reverse,start,end,top,bottom;if(properties.
if(reverse){top='end';bottom='start';}else{top='start';bottom='end';}
return{start,end,reverse,top,bottom};}
function setBorderSkipped(properties,options,stack,index){let edge=options.borderSkipped;const res={};if(!edge){properties.borderSkipped=res;return;}
if(edge===true){properties.borderSkipped={top:true,right:true,bottom:true,left:true};return;}
const{start,end,reverse,top,bottom}=borderProps(properties);if(edge==='middle'&&stack){properties.enableBorderRadius=true;if((stack._top||0)===index){edge=top;}else if((stack._bottom||0)===index){edge=bottom;}else{res[parseEdge(bottom,start,end,reverse)]=true;edge=top;}}
res[parseEdge(edge,start,end,reverse)]=true;properties.borderSkipped=res;}
function parseEdge(edge,a,b,reverse){if(reverse){edge=swap(edge,a,b);edge=startEnd(edge,b,a);}else{edge=startEnd(edge,a,b);}
@ -2166,7 +2176,7 @@ _getVisibleDatasetWeightTotal(){return this._getRingWeightOffset(this.chart.data
DoughnutController.id='doughnut';DoughnutController.defaults={datasetElementType:false,dataElementType:'arc',animation:{animateRotate:true,animateScale:false},animations:{numbers:{type:'number',properties:['circumference','endAngle','innerRadius','outerRadius','startAngle','x','y','offset','borderWidth','spacing']},},cutout:'50%',rotation:0,circumference:360,radius:'100%',spacing:0,indexAxis:'r',};DoughnutController.descriptors={_scriptable:(name)=>name!=='spacing',_indexable:(name)=>name!=='spacing',};DoughnutController.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(chart){const data=chart.data;if(data.labels.length&&data.datasets.length){const{labels:{pointStyle}}=chart.legend.options;return data.labels.map((label,i)=>{const meta=chart.getDatasetMeta(0);const style=meta.controller.getStyle(i);return{text:label,fillStyle:style.backgroundColor,strokeStyle:style.borderColor,lineWidth:style.borderWidth,pointStyle:pointStyle,hidden:!chart.getDataVisibility(i),index:i};});}
return[];}},onClick(e,legendItem,legend){legend.chart.toggleDataVisibility(legendItem.index);legend.chart.update();}},tooltip:{callbacks:{title(){return'';},label(tooltipItem){let dataLabel=tooltipItem.label;const value=': '+tooltipItem.formattedValue;if(isArray(dataLabel)){dataLabel=dataLabel.slice();dataLabel[0]+=value;}else{dataLabel+=value;}
return dataLabel;}}}}};class LineController extends DatasetController{initialize(){this.enableOptionSharing=true;this.supportsDecimation=true;super.initialize();}
update(mode){const meta=this._cachedMeta;const{dataset:line,data:points=[],_dataset}=meta;const animationsDisabled=this.chart._animationsDisabled;let{start,count}=getStartAndCountOfVisiblePoints(meta,points,animationsDisabled);this._drawStart=start;this._drawCount=count;if(scaleRangesChanged(meta)){start=0;count=points.length;}
update(mode){const meta=this._cachedMeta;const{dataset:line,data:points=[],_dataset}=meta;const animationsDisabled=this.chart._animationsDisabled;let{start,count}=_getStartAndCountOfVisiblePoints(meta,points,animationsDisabled);this._drawStart=start;this._drawCount=count;if(_scaleRangesChanged(meta)){start=0;count=points.length;}
line._chart=this.chart;line._datasetIndex=this.index;line._decimated=!!_dataset._decimated;line.points=points;const options=this.resolveDatasetElementOptions(mode);if(!this.options.showLine){options.borderWidth=0;}
options.segment=this.options.segment;this.updateElement(line,undefined,{animated:!animationsDisabled,options},mode);this.updateElements(points,start,count,mode);}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const{sharedOptions,includeOptions}=this._getSharedOptions(start,mode);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.getParsed(i);const properties=directUpdate?point:{};const nullData=isNullOrUndef(parsed[vAxis]);const iPixel=properties[iAxis]=iScale.getPixelForValue(parsed[iAxis],i);const vPixel=properties[vAxis]=reset||nullData?vScale.getBasePixel():vScale.getPixelForValue(_stacked?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(Math.abs(parsed[iAxis]-prevParsed[iAxis]))>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
@ -2176,12 +2186,7 @@ prevParsed=parsed;}}
getMaxOverflow(){const meta=this._cachedMeta;const dataset=meta.dataset;const border=dataset.options&&dataset.options.borderWidth||0;const data=meta.data||[];if(!data.length){return border;}
const firstPoint=data[0].size(this.resolveDataElementOptions(0));const lastPoint=data[data.length-1].size(this.resolveDataElementOptions(data.length-1));return Math.max(border,firstPoint,lastPoint)/2;}
draw(){const meta=this._cachedMeta;meta.dataset.updateControlPoints(this.chart.chartArea,meta.iScale.axis);super.draw();}}
LineController.id='line';LineController.defaults={datasetElementType:'line',dataElementType:'point',showLine:true,spanGaps:false,};LineController.overrides={scales:{_index_:{type:'category',},_value_:{type:'linear',},}};function getStartAndCountOfVisiblePoints(meta,points,animationsDisabled){const pointCount=points.length;let start=0;let count=pointCount;if(meta._sorted){const{iScale,_parsed}=meta;const axis=iScale.axis;const{min,max,minDefined,maxDefined}=iScale.getUserBounds();if(minDefined){start=_limitValue(Math.min(_lookupByKey(_parsed,iScale.axis,min).lo,animationsDisabled?pointCount:_lookupByKey(points,axis,iScale.getPixelForValue(min)).lo),0,pointCount-1);}
if(maxDefined){count=_limitValue(Math.max(_lookupByKey(_parsed,iScale.axis,max).hi+1,animationsDisabled?0:_lookupByKey(points,axis,iScale.getPixelForValue(max)).hi+1),start,pointCount)-start;}else{count=pointCount-start;}}
return{start,count};}
function scaleRangesChanged(meta){const{xScale,yScale,_scaleRanges}=meta;const newRanges={xmin:xScale.min,xmax:xScale.max,ymin:yScale.min,ymax:yScale.max};if(!_scaleRanges){meta._scaleRanges=newRanges;return true;}
const changed=_scaleRanges.xmin!==xScale.min||_scaleRanges.xmax!==xScale.max||_scaleRanges.ymin!==yScale.min||_scaleRanges.ymax!==yScale.max;Object.assign(_scaleRanges,newRanges);return changed;}
class PolarAreaController extends DatasetController{constructor(chart,datasetIndex){super(chart,datasetIndex);this.innerRadius=undefined;this.outerRadius=undefined;}
LineController.id='line';LineController.defaults={datasetElementType:'line',dataElementType:'point',showLine:true,spanGaps:false,};LineController.overrides={scales:{_index_:{type:'category',},_value_:{type:'linear',},}};class PolarAreaController extends DatasetController{constructor(chart,datasetIndex){super(chart,datasetIndex);this.innerRadius=undefined;this.outerRadius=undefined;}
getLabelAndValue(index){const meta=this._cachedMeta;const chart=this.chart;const labels=chart.data.labels||[];const value=formatNumber(meta._parsed[index].r,chart.options.locale);return{label:labels[index]||'',value,};}
parseObjectData(meta,data,start,count){return _parseObjectDataRadialScale.bind(this)(meta,data,start,count);}
update(mode){const arcs=this._cachedMeta.data;this._updateRadius();this.updateElements(arcs,0,arcs.length,mode);}
@ -2202,37 +2207,50 @@ update(mode){const meta=this._cachedMeta;const line=meta.dataset;const points=me
const properties={_loop:true,_fullLoop:labels.length===points.length,options};this.updateElement(line,undefined,properties,mode);}
this.updateElements(points,0,points.length,mode);}
updateElements(points,start,count,mode){const scale=this._cachedMeta.rScale;const reset=mode==='reset';for(let i=start;i<start+count;i++){const point=points[i];const options=this.resolveDataElementOptions(i,point.active?'active':mode);const pointPosition=scale.getPointPositionForValue(i,this.getParsed(i).r);const x=reset?scale.xCenter:pointPosition.x;const y=reset?scale.yCenter:pointPosition.y;const properties={x,y,angle:pointPosition.angle,skip:isNaN(x)||isNaN(y),options};this.updateElement(point,i,properties,mode);}}}
RadarController.id='radar';RadarController.defaults={datasetElementType:'line',dataElementType:'point',indexAxis:'r',showLine:true,elements:{line:{fill:'start'}},};RadarController.overrides={aspectRatio:1,scales:{r:{type:'radialLinear',}}};class ScatterController extends LineController{}
ScatterController.id='scatter';ScatterController.defaults={showLine:false,fill:false};ScatterController.overrides={interaction:{mode:'point'},plugins:{tooltip:{callbacks:{title(){return'';},label(item){return'('+item.label+', '+item.formattedValue+')';}}}},scales:{x:{type:'linear'},y:{type:'linear'}}};var controllers=Object.freeze({__proto__:null,BarController:BarController,BubbleController:BubbleController,DoughnutController:DoughnutController,LineController:LineController,PolarAreaController:PolarAreaController,PieController:PieController,RadarController:RadarController,ScatterController:ScatterController});function clipArc(ctx,element,endAngle){const{startAngle,pixelMargin,x,y,outerRadius,innerRadius}=element;let angleMargin=pixelMargin/outerRadius;ctx.beginPath();ctx.arc(x,y,outerRadius,startAngle-angleMargin,endAngle+angleMargin);if(innerRadius>pixelMargin){angleMargin=pixelMargin/innerRadius;ctx.arc(x,y,innerRadius,endAngle+angleMargin,startAngle-angleMargin,true);}else{ctx.arc(x,y,pixelMargin,endAngle+HALF_PI,startAngle-HALF_PI);}
RadarController.id='radar';RadarController.defaults={datasetElementType:'line',dataElementType:'point',indexAxis:'r',showLine:true,elements:{line:{fill:'start'}},};RadarController.overrides={aspectRatio:1,scales:{r:{type:'radialLinear',}}};class ScatterController extends DatasetController{update(mode){const meta=this._cachedMeta;const{data:points=[]}=meta;const animationsDisabled=this.chart._animationsDisabled;let{start,count}=_getStartAndCountOfVisiblePoints(meta,points,animationsDisabled);this._drawStart=start;this._drawCount=count;if(_scaleRangesChanged(meta)){start=0;count=points.length;}
if(this.options.showLine){const{dataset:line,_dataset}=meta;line._chart=this.chart;line._datasetIndex=this.index;line._decimated=!!_dataset._decimated;line.points=points;const options=this.resolveDatasetElementOptions(mode);options.segment=this.options.segment;this.updateElement(line,undefined,{animated:!animationsDisabled,options},mode);}
this.updateElements(points,start,count,mode);}
addElements(){const{showLine}=this.options;if(!this.datasetElementType&&showLine){this.datasetElementType=registry.getElement('line');}
super.addElements();}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const firstOpts=this.resolveDataElementOptions(start,mode);const sharedOptions=this.getSharedOptions(firstOpts);const includeOptions=this.includeOptions(mode,sharedOptions);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.getParsed(i);const properties=directUpdate?point:{};const nullData=isNullOrUndef(parsed[vAxis]);const iPixel=properties[iAxis]=iScale.getPixelForValue(parsed[iAxis],i);const vPixel=properties[vAxis]=reset||nullData?vScale.getBasePixel():vScale.getPixelForValue(_stacked?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(Math.abs(parsed[iAxis]-prevParsed[iAxis]))>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
if(includeOptions){properties.options=sharedOptions||this.resolveDataElementOptions(i,point.active?'active':mode);}
if(!directUpdate){this.updateElement(point,i,properties,mode);}
prevParsed=parsed;}
this.updateSharedOptions(sharedOptions,mode,firstOpts);}
getMaxOverflow(){const meta=this._cachedMeta;const data=meta.data||[];if(!this.options.showLine){let max=0;for(let i=data.length-1;i>=0;--i){max=Math.max(max,data[i].size(this.resolveDataElementOptions(i))/2);}
return max>0&&max;}
const dataset=meta.dataset;const border=dataset.options&&dataset.options.borderWidth||0;if(!data.length){return border;}
const firstPoint=data[0].size(this.resolveDataElementOptions(0));const lastPoint=data[data.length-1].size(this.resolveDataElementOptions(data.length-1));return Math.max(border,firstPoint,lastPoint)/2;}}
ScatterController.id='scatter';ScatterController.defaults={datasetElementType:false,dataElementType:'point',showLine:false,fill:false};ScatterController.overrides={interaction:{mode:'point'},plugins:{tooltip:{callbacks:{title(){return'';},label(item){return'('+item.label+', '+item.formattedValue+')';}}}},scales:{x:{type:'linear'},y:{type:'linear'}}};var controllers=Object.freeze({__proto__:null,BarController:BarController,BubbleController:BubbleController,DoughnutController:DoughnutController,LineController:LineController,PolarAreaController:PolarAreaController,PieController:PieController,RadarController:RadarController,ScatterController:ScatterController});function clipArc(ctx,element,endAngle){const{startAngle,pixelMargin,x,y,outerRadius,innerRadius}=element;let angleMargin=pixelMargin/outerRadius;ctx.beginPath();ctx.arc(x,y,outerRadius,startAngle-angleMargin,endAngle+angleMargin);if(innerRadius>pixelMargin){angleMargin=pixelMargin/innerRadius;ctx.arc(x,y,innerRadius,endAngle+angleMargin,startAngle-angleMargin,true);}else{ctx.arc(x,y,pixelMargin,endAngle+HALF_PI,startAngle-HALF_PI);}
ctx.closePath();ctx.clip();}
function toRadiusCorners(value){return _readValueToProps(value,['outerStart','outerEnd','innerStart','innerEnd']);}
function parseBorderRadius$1(arc,innerRadius,outerRadius,angleDelta){const o=toRadiusCorners(arc.options.borderRadius);const halfThickness=(outerRadius-innerRadius)/2;const innerLimit=Math.min(halfThickness,angleDelta*innerRadius/2);const computeOuterLimit=(val)=>{const outerArcLimit=(outerRadius-Math.min(halfThickness,val))*angleDelta/2;return _limitValue(val,0,Math.min(halfThickness,outerArcLimit));};return{outerStart:computeOuterLimit(o.outerStart),outerEnd:computeOuterLimit(o.outerEnd),innerStart:_limitValue(o.innerStart,0,innerLimit),innerEnd:_limitValue(o.innerEnd,0,innerLimit),};}
function rThetaToXY(r,theta,x,y){return{x:x+r*Math.cos(theta),y:y+r*Math.sin(theta),};}
function pathArc(ctx,element,offset,spacing,end){const{x,y,startAngle:start,pixelMargin,innerRadius:innerR}=element;const outerRadius=Math.max(element.outerRadius+spacing+offset-pixelMargin,0);const innerRadius=innerR>0?innerR+spacing+offset+pixelMargin:0;let spacingOffset=0;const alpha=end-start;if(spacing){const noSpacingInnerRadius=innerR>0?innerR-spacing:0;const noSpacingOuterRadius=outerRadius>0?outerRadius-spacing:0;const avNogSpacingRadius=(noSpacingInnerRadius+noSpacingOuterRadius)/2;const adjustedAngle=avNogSpacingRadius!==0?(alpha*avNogSpacingRadius)/(avNogSpacingRadius+spacing):alpha;spacingOffset=(alpha-adjustedAngle)/2;}
const beta=Math.max(0.001,alpha*outerRadius-offset/PI)/outerRadius;const angleOffset=(alpha-beta)/2;const startAngle=start+angleOffset+spacingOffset;const endAngle=end-angleOffset-spacingOffset;const{outerStart,outerEnd,innerStart,innerEnd}=parseBorderRadius$1(element,innerRadius,outerRadius,endAngle-startAngle);const outerStartAdjustedRadius=outerRadius-outerStart;const outerEndAdjustedRadius=outerRadius-outerEnd;const outerStartAdjustedAngle=startAngle+outerStart/outerStartAdjustedRadius;const outerEndAdjustedAngle=endAngle-outerEnd/outerEndAdjustedRadius;const innerStartAdjustedRadius=innerRadius+innerStart;const innerEndAdjustedRadius=innerRadius+innerEnd;const innerStartAdjustedAngle=startAngle+innerStart/innerStartAdjustedRadius;const innerEndAdjustedAngle=endAngle-innerEnd/innerEndAdjustedRadius;ctx.beginPath();ctx.arc(x,y,outerRadius,outerStartAdjustedAngle,outerEndAdjustedAngle);if(outerEnd>0){const pCenter=rThetaToXY(outerEndAdjustedRadius,outerEndAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,outerEnd,outerEndAdjustedAngle,endAngle+HALF_PI);}
function pathArc(ctx,element,offset,spacing,end,circular){const{x,y,startAngle:start,pixelMargin,innerRadius:innerR}=element;const outerRadius=Math.max(element.outerRadius+spacing+offset-pixelMargin,0);const innerRadius=innerR>0?innerR+spacing+offset+pixelMargin:0;let spacingOffset=0;const alpha=end-start;if(spacing){const noSpacingInnerRadius=innerR>0?innerR-spacing:0;const noSpacingOuterRadius=outerRadius>0?outerRadius-spacing:0;const avNogSpacingRadius=(noSpacingInnerRadius+noSpacingOuterRadius)/2;const adjustedAngle=avNogSpacingRadius!==0?(alpha*avNogSpacingRadius)/(avNogSpacingRadius+spacing):alpha;spacingOffset=(alpha-adjustedAngle)/2;}
const beta=Math.max(0.001,alpha*outerRadius-offset/PI)/outerRadius;const angleOffset=(alpha-beta)/2;const startAngle=start+angleOffset+spacingOffset;const endAngle=end-angleOffset-spacingOffset;const{outerStart,outerEnd,innerStart,innerEnd}=parseBorderRadius$1(element,innerRadius,outerRadius,endAngle-startAngle);const outerStartAdjustedRadius=outerRadius-outerStart;const outerEndAdjustedRadius=outerRadius-outerEnd;const outerStartAdjustedAngle=startAngle+outerStart/outerStartAdjustedRadius;const outerEndAdjustedAngle=endAngle-outerEnd/outerEndAdjustedRadius;const innerStartAdjustedRadius=innerRadius+innerStart;const innerEndAdjustedRadius=innerRadius+innerEnd;const innerStartAdjustedAngle=startAngle+innerStart/innerStartAdjustedRadius;const innerEndAdjustedAngle=endAngle-innerEnd/innerEndAdjustedRadius;ctx.beginPath();if(circular){ctx.arc(x,y,outerRadius,outerStartAdjustedAngle,outerEndAdjustedAngle);if(outerEnd>0){const pCenter=rThetaToXY(outerEndAdjustedRadius,outerEndAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,outerEnd,outerEndAdjustedAngle,endAngle+HALF_PI);}
const p4=rThetaToXY(innerEndAdjustedRadius,endAngle,x,y);ctx.lineTo(p4.x,p4.y);if(innerEnd>0){const pCenter=rThetaToXY(innerEndAdjustedRadius,innerEndAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,innerEnd,endAngle+HALF_PI,innerEndAdjustedAngle+Math.PI);}
ctx.arc(x,y,innerRadius,endAngle-(innerEnd/innerRadius),startAngle+(innerStart/innerRadius),true);if(innerStart>0){const pCenter=rThetaToXY(innerStartAdjustedRadius,innerStartAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,innerStart,innerStartAdjustedAngle+Math.PI,startAngle-HALF_PI);}
const p8=rThetaToXY(outerStartAdjustedRadius,startAngle,x,y);ctx.lineTo(p8.x,p8.y);if(outerStart>0){const pCenter=rThetaToXY(outerStartAdjustedRadius,outerStartAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,outerStart,startAngle-HALF_PI,outerStartAdjustedAngle);}
const p8=rThetaToXY(outerStartAdjustedRadius,startAngle,x,y);ctx.lineTo(p8.x,p8.y);if(outerStart>0){const pCenter=rThetaToXY(outerStartAdjustedRadius,outerStartAdjustedAngle,x,y);ctx.arc(pCenter.x,pCenter.y,outerStart,startAngle-HALF_PI,outerStartAdjustedAngle);}}else{ctx.moveTo(x,y);const outerStartX=Math.cos(outerStartAdjustedAngle)*outerRadius+x;const outerStartY=Math.sin(outerStartAdjustedAngle)*outerRadius+y;ctx.lineTo(outerStartX,outerStartY);const outerEndX=Math.cos(outerEndAdjustedAngle)*outerRadius+x;const outerEndY=Math.sin(outerEndAdjustedAngle)*outerRadius+y;ctx.lineTo(outerEndX,outerEndY);}
ctx.closePath();}
function drawArc(ctx,element,offset,spacing){const{fullCircles,startAngle,circumference}=element;let endAngle=element.endAngle;if(fullCircles){pathArc(ctx,element,offset,spacing,startAngle+TAU);for(let i=0;i<fullCircles;++i){ctx.fill();}
function drawArc(ctx,element,offset,spacing,circular){const{fullCircles,startAngle,circumference}=element;let endAngle=element.endAngle;if(fullCircles){pathArc(ctx,element,offset,spacing,startAngle+TAU,circular);for(let i=0;i<fullCircles;++i){ctx.fill();}
if(!isNaN(circumference)){endAngle=startAngle+circumference%TAU;if(circumference%TAU===0){endAngle+=TAU;}}}
pathArc(ctx,element,offset,spacing,endAngle);ctx.fill();return endAngle;}
pathArc(ctx,element,offset,spacing,endAngle,circular);ctx.fill();return endAngle;}
function drawFullCircleBorders(ctx,element,inner){const{x,y,startAngle,pixelMargin,fullCircles}=element;const outerRadius=Math.max(element.outerRadius-pixelMargin,0);const innerRadius=element.innerRadius+pixelMargin;let i;if(inner){clipArc(ctx,element,startAngle+TAU);}
ctx.beginPath();ctx.arc(x,y,innerRadius,startAngle+TAU,startAngle,true);for(i=0;i<fullCircles;++i){ctx.stroke();}
ctx.beginPath();ctx.arc(x,y,outerRadius,startAngle,startAngle+TAU);for(i=0;i<fullCircles;++i){ctx.stroke();}}
function drawBorder(ctx,element,offset,spacing,endAngle){const{options}=element;const{borderWidth,borderJoinStyle}=options;const inner=options.borderAlign==='inner';if(!borderWidth){return;}
function drawBorder(ctx,element,offset,spacing,endAngle,circular){const{options}=element;const{borderWidth,borderJoinStyle}=options;const inner=options.borderAlign==='inner';if(!borderWidth){return;}
if(inner){ctx.lineWidth=borderWidth*2;ctx.lineJoin=borderJoinStyle||'round';}else{ctx.lineWidth=borderWidth;ctx.lineJoin=borderJoinStyle||'bevel';}
if(element.fullCircles){drawFullCircleBorders(ctx,element,inner);}
if(inner){clipArc(ctx,element,endAngle);}
pathArc(ctx,element,offset,spacing,endAngle);ctx.stroke();}
pathArc(ctx,element,offset,spacing,endAngle,circular);ctx.stroke();}
class ArcElement extends Element{constructor(cfg){super();this.options=undefined;this.circumference=undefined;this.startAngle=undefined;this.endAngle=undefined;this.innerRadius=undefined;this.outerRadius=undefined;this.pixelMargin=0;this.fullCircles=0;if(cfg){Object.assign(this,cfg);}}
inRange(chartX,chartY,useFinalPosition){const point=this.getProps(['x','y'],useFinalPosition);const{angle,distance}=getAngleFromPoint(point,{x:chartX,y:chartY});const{startAngle,endAngle,innerRadius,outerRadius,circumference}=this.getProps(['startAngle','endAngle','innerRadius','outerRadius','circumference'],useFinalPosition);const rAdjust=this.options.spacing/2;const _circumference=valueOrDefault(circumference,endAngle-startAngle);const betweenAngles=_circumference>=TAU||_angleBetween(angle,startAngle,endAngle);const withinRadius=_isBetween(distance,innerRadius+rAdjust,outerRadius+rAdjust);return(betweenAngles&&withinRadius);}
getCenterPoint(useFinalPosition){const{x,y,startAngle,endAngle,innerRadius,outerRadius}=this.getProps(['x','y','startAngle','endAngle','innerRadius','outerRadius','circumference',],useFinalPosition);const{offset,spacing}=this.options;const halfAngle=(startAngle+endAngle)/2;const halfRadius=(innerRadius+outerRadius+spacing+offset)/2;return{x:x+Math.cos(halfAngle)*halfRadius,y:y+Math.sin(halfAngle)*halfRadius};}
tooltipPosition(useFinalPosition){return this.getCenterPoint(useFinalPosition);}
draw(ctx){const{options,circumference}=this;const offset=(options.offset||0)/2;const spacing=(options.spacing||0)/2;this.pixelMargin=(options.borderAlign==='inner')?0.33:0;this.fullCircles=circumference>TAU?Math.floor(circumference/TAU):0;if(circumference===0||this.innerRadius<0||this.outerRadius<0){return;}
draw(ctx){const{options,circumference}=this;const offset=(options.offset||0)/2;const spacing=(options.spacing||0)/2;const circular=options.circular;this.pixelMargin=(options.borderAlign==='inner')?0.33:0;this.fullCircles=circumference>TAU?Math.floor(circumference/TAU):0;if(circumference===0||this.innerRadius<0||this.outerRadius<0){return;}
ctx.save();let radiusOffset=0;if(offset){radiusOffset=offset/2;const halfAngle=(this.startAngle+this.endAngle)/2;ctx.translate(Math.cos(halfAngle)*radiusOffset,Math.sin(halfAngle)*radiusOffset);if(this.circumference>=PI){radiusOffset=offset;}}
ctx.fillStyle=options.backgroundColor;ctx.strokeStyle=options.borderColor;const endAngle=drawArc(ctx,this,radiusOffset,spacing);drawBorder(ctx,this,radiusOffset,spacing,endAngle);ctx.restore();}}
ArcElement.id='arc';ArcElement.defaults={borderAlign:'center',borderColor:'#fff',borderJoinStyle:undefined,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:undefined,};ArcElement.defaultRoutes={backgroundColor:'backgroundColor'};function setStyle(ctx,options,style=options){ctx.lineCap=valueOrDefault(style.borderCapStyle,options.borderCapStyle);ctx.setLineDash(valueOrDefault(style.borderDash,options.borderDash));ctx.lineDashOffset=valueOrDefault(style.borderDashOffset,options.borderDashOffset);ctx.lineJoin=valueOrDefault(style.borderJoinStyle,options.borderJoinStyle);ctx.lineWidth=valueOrDefault(style.borderWidth,options.borderWidth);ctx.strokeStyle=valueOrDefault(style.borderColor,options.borderColor);}
ctx.fillStyle=options.backgroundColor;ctx.strokeStyle=options.borderColor;const endAngle=drawArc(ctx,this,radiusOffset,spacing,circular);drawBorder(ctx,this,radiusOffset,spacing,endAngle,circular);ctx.restore();}}
ArcElement.id='arc';ArcElement.defaults={borderAlign:'center',borderColor:'#fff',borderJoinStyle:undefined,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:undefined,circular:true,};ArcElement.defaultRoutes={backgroundColor:'backgroundColor'};function setStyle(ctx,options,style=options){ctx.lineCap=valueOrDefault(style.borderCapStyle,options.borderCapStyle);ctx.setLineDash(valueOrDefault(style.borderDash,options.borderDash));ctx.lineDashOffset=valueOrDefault(style.borderDashOffset,options.borderDashOffset);ctx.lineJoin=valueOrDefault(style.borderJoinStyle,options.borderJoinStyle);ctx.lineWidth=valueOrDefault(style.borderWidth,options.borderWidth);ctx.strokeStyle=valueOrDefault(style.borderColor,options.borderColor);}
function lineTo(ctx,previous,target){ctx.lineTo(target.x,target.y);}
function getLineMethod(options){if(options.stepped){return _steppedLineTo;}
if(options.tension||options.cubicInterpolationMode==='monotone'){return _bezierCurveTo;}
@ -2426,7 +2444,7 @@ hitbox.top=top;hitbox.left+=this.left+padding;hitbox.left=rtlHelper.leftForLtr(r
isHorizontal(){return this.options.position==='top'||this.options.position==='bottom';}
draw(){if(this.options.display){const ctx=this.ctx;clipArea(ctx,this);this._draw();unclipArea(ctx);}}
_draw(){const{options:opts,columnSizes,lineWidths,ctx}=this;const{align,labels:labelOpts}=opts;const defaultColor=defaults.color;const rtlHelper=getRtlAdapter(opts.rtl,this.left,this.width);const labelFont=toFont(labelOpts.font);const{color:fontColor,padding}=labelOpts;const fontSize=labelFont.size;const halfFontSize=fontSize/2;let cursor;this.drawTitle();ctx.textAlign=rtlHelper.textAlign('left');ctx.textBaseline='middle';ctx.lineWidth=0.5;ctx.font=labelFont.string;const{boxWidth,boxHeight,itemHeight}=getBoxSize(labelOpts,fontSize);const drawLegendBox=function(x,y,legendItem){if(isNaN(boxWidth)||boxWidth<=0||isNaN(boxHeight)||boxHeight<0){return;}
ctx.save();const lineWidth=valueOrDefault(legendItem.lineWidth,1);ctx.fillStyle=valueOrDefault(legendItem.fillStyle,defaultColor);ctx.lineCap=valueOrDefault(legendItem.lineCap,'butt');ctx.lineDashOffset=valueOrDefault(legendItem.lineDashOffset,0);ctx.lineJoin=valueOrDefault(legendItem.lineJoin,'miter');ctx.lineWidth=lineWidth;ctx.strokeStyle=valueOrDefault(legendItem.strokeStyle,defaultColor);ctx.setLineDash(valueOrDefault(legendItem.lineDash,[]));if(labelOpts.usePointStyle){const drawOptions={radius:boxHeight*Math.SQRT2/2,pointStyle:legendItem.pointStyle,rotation:legendItem.rotation,borderWidth:lineWidth};const centerX=rtlHelper.xPlus(x,boxWidth/2);const centerY=y+halfFontSize;drawPointLegend(ctx,drawOptions,centerX,centerY,boxWidth);}else{const yBoxTop=y+Math.max((fontSize-boxHeight)/2,0);const xBoxLeft=rtlHelper.leftForLtr(x,boxWidth);const borderRadius=toTRBLCorners(legendItem.borderRadius);ctx.beginPath();if(Object.values(borderRadius).some(v=>v!==0)){addRoundedRectPath(ctx,{x:xBoxLeft,y:yBoxTop,w:boxWidth,h:boxHeight,radius:borderRadius,});}else{ctx.rect(xBoxLeft,yBoxTop,boxWidth,boxHeight);}
ctx.save();const lineWidth=valueOrDefault(legendItem.lineWidth,1);ctx.fillStyle=valueOrDefault(legendItem.fillStyle,defaultColor);ctx.lineCap=valueOrDefault(legendItem.lineCap,'butt');ctx.lineDashOffset=valueOrDefault(legendItem.lineDashOffset,0);ctx.lineJoin=valueOrDefault(legendItem.lineJoin,'miter');ctx.lineWidth=lineWidth;ctx.strokeStyle=valueOrDefault(legendItem.strokeStyle,defaultColor);ctx.setLineDash(valueOrDefault(legendItem.lineDash,[]));if(labelOpts.usePointStyle){const drawOptions={radius:boxHeight*Math.SQRT2/2,pointStyle:legendItem.pointStyle,rotation:legendItem.rotation,borderWidth:lineWidth};const centerX=rtlHelper.xPlus(x,boxWidth/2);const centerY=y+halfFontSize;drawPointLegend(ctx,drawOptions,centerX,centerY,labelOpts.pointStyleWidth&&boxWidth);}else{const yBoxTop=y+Math.max((fontSize-boxHeight)/2,0);const xBoxLeft=rtlHelper.leftForLtr(x,boxWidth);const borderRadius=toTRBLCorners(legendItem.borderRadius);ctx.beginPath();if(Object.values(borderRadius).some(v=>v!==0)){addRoundedRectPath(ctx,{x:xBoxLeft,y:yBoxTop,w:boxWidth,h:boxHeight,radius:borderRadius,});}else{ctx.rect(xBoxLeft,yBoxTop,boxWidth,boxHeight);}
ctx.fill();if(lineWidth!==0){ctx.stroke();}}
ctx.restore();};const fillText=function(x,y,legendItem){renderText(ctx,legendItem.text,x,y+(itemHeight/2),labelFont,{strikethrough:legendItem.hidden,textAlign:rtlHelper.textAlign(legendItem.textAlign)});};const isHorizontal=this.isHorizontal();const titleHeight=this._computeTitleHeight();if(isHorizontal){cursor={x:_alignStartEnd(align,this.left+padding,this.right-lineWidths[0]),y:this.top+padding+titleHeight,line:0};}else{cursor={x:this.left+padding,y:_alignStartEnd(align,this.top+titleHeight+padding,this.bottom-columnSizes[0].height),line:0};}
overrideTextDirection(this.ctx,opts.textDirection);const lineHeight=itemHeight+padding;this.legendItems.forEach((legendItem,i)=>{ctx.strokeStyle=legendItem.fontColor||fontColor;ctx.fillStyle=legendItem.fontColor||fontColor;const textWidth=ctx.measureText(legendItem.text).width;const textAlign=rtlHelper.textAlign(legendItem.textAlign||(legendItem.textAlign=labelOpts.textAlign));const width=boxWidth+halfFontSize+textWidth;let x=cursor.x;let y=cursor.y;rtlHelper.setWidth(this.width);if(isHorizontal){if(i>0&&x+width+padding>this.right){y=cursor.y+=lineHeight;cursor.line++;x=cursor.x=_alignStartEnd(align,this.left+padding,this.right-lineWidths[cursor.line]);}}else if(i>0&&y+lineHeight>this.bottom){x=cursor.x=x+columnSizes[cursor.line].width+padding;cursor.line++;y=cursor.y=_alignStartEnd(align,this.top+titleHeight+padding,this.bottom-columnSizes[cursor.line].height);}
@ -2698,7 +2716,7 @@ return ticks;}
function ticksFromTimestamps(scale,values,majorUnit){const ticks=[];const map={};const ilen=values.length;let i,value;for(i=0;i<ilen;++i){value=values[i];map[value]=i;ticks.push({value,major:false});}
return(ilen===0||!majorUnit)?ticks:setMajorTicks(scale,ticks,map,majorUnit);}
class TimeScale extends Scale{constructor(props){super(props);this._cache={data:[],labels:[],all:[]};this._unit='day';this._majorUnit=undefined;this._offsets={};this._normalized=false;this._parseOpts=undefined;}
init(scaleOpts,opts){const time=scaleOpts.time||(scaleOpts.time={});const adapter=this._adapter=new _adapters._date(scaleOpts.adapters.date);mergeIf(time.displayFormats,adapter.formats());this._parseOpts={parser:time.parser,round:time.round,isoWeekday:time.isoWeekday};super.init(scaleOpts);this._normalized=opts.normalized;}
init(scaleOpts,opts){const time=scaleOpts.time||(scaleOpts.time={});const adapter=this._adapter=new _adapters._date(scaleOpts.adapters.date);adapter.init(opts);mergeIf(time.displayFormats,adapter.formats());this._parseOpts={parser:time.parser,round:time.round,isoWeekday:time.isoWeekday};super.init(scaleOpts);this._normalized=opts.normalized;}
parse(raw,index){if(raw===undefined){return null;}
return parse(this,raw);}
beforeLayout(){super.beforeLayout();this._cache={data:[],labels:[],all:[]};}

File diff suppressed because one or more lines are too long

View file

@ -643,23 +643,23 @@
* /account/verification/phone](/docs/client/account#accountCreatePhoneVerification)
* endpoint to send a confirmation SMS.
*
* @param {string} number
* @param {string} phone
* @param {string} password
* @throws {AppwriteException}
* @returns {Promise}
*/
updatePhone(number, password) {
updatePhone(phone, password) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof number === 'undefined') {
throw new AppwriteException('Missing required parameter: "number"');
if (typeof phone === 'undefined') {
throw new AppwriteException('Missing required parameter: "phone"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/account/phone';
let payload = {};
if (typeof number !== 'undefined') {
payload['number'] = number;
if (typeof phone !== 'undefined') {
payload['phone'] = phone;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
@ -1051,25 +1051,25 @@
* is valid for 15 minutes.
*
* @param {string} userId
* @param {string} number
* @param {string} phone
* @throws {AppwriteException}
* @returns {Promise}
*/
createPhoneSession(userId, number) {
createPhoneSession(userId, phone) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof number === 'undefined') {
throw new AppwriteException('Missing required parameter: "number"');
if (typeof phone === 'undefined') {
throw new AppwriteException('Missing required parameter: "phone"');
}
let path = '/account/sessions/phone';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof number !== 'undefined') {
payload['number'] = number;
if (typeof phone !== 'undefined') {
payload['phone'] = phone;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
@ -1078,7 +1078,7 @@
});
}
/**
* Create Phone session (confirmation)
* Create Phone Session (confirmation)
*
* Use this endpoint to complete creating a session with SMS. Use the
* **userId** from the
@ -3183,6 +3183,27 @@
}, payload);
});
}
/**
* Get Functions Usage
*
*
* @param {string} range
* @throws {AppwriteException}
* @returns {Promise}
*/
getUsage(range) {
return __awaiter(this, void 0, void 0, function* () {
let path = '/functions/usage';
let payload = {};
if (typeof range !== 'undefined') {
payload['range'] = range;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('get', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Get Function
*
@ -3633,7 +3654,7 @@
* @throws {AppwriteException}
* @returns {Promise}
*/
getUsage(functionId, range) {
getFunctionUsage(functionId, range) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof functionId === 'undefined') {
throw new AppwriteException('Missing required parameter: "functionId"');
@ -6301,22 +6322,17 @@
*
* @param {string} userId
* @param {string} email
* @param {string} phone
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
create(userId, email, password, name) {
create(userId, email, phone, password, name) {
return __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"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users';
let payload = {};
if (typeof userId !== 'undefined') {
@ -6325,6 +6341,9 @@
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof phone !== 'undefined') {
payload['phone'] = phone;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}

View file

@ -232,6 +232,13 @@ window.ls.router
scope: "console",
project: true
})
.add("/console/functions/usage", {
template: function(window) {
return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER;
},
scope: "console",
project: true
})
.add("/console/functions/function", {
template: "/console/functions/function?version=" + APP_ENV.CACHEBUSTER,
scope: "console",

View file

@ -1,200 +0,0 @@
<?php
namespace Appwrite\Stats;
use Utopia\App;
class Stats
{
/**
* @var array
*/
protected $params = [];
/**
* @var mixed
*/
protected $statsd;
/**
* @var string
*/
protected $namespace = 'appwrite.usage';
/**
* Event constructor.
*
* @param mixed $statsd
*/
public function __construct($statsd)
{
$this->statsd = $statsd;
}
/**
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setParam(string $key, $value): self
{
$this->params[$key] = $value;
return $this;
}
/**
* @param string $key
*
* @return mixed|null
*/
public function getParam(string $key)
{
return (isset($this->params[$key])) ? $this->params[$key] : null;
}
/**
* @param string $namespace
*
* @return $this
*/
public function setNamespace(string $namespace): self
{
$this->namespace = $namespace;
return $this;
}
/**
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Submit data to StatsD.
*/
public function submit(): void
{
$projectId = $this->params['projectId'] ?? '';
$storage = $this->params['storage'] ?? 0;
$networkRequestSize = $this->params['networkRequestSize'] ?? 0;
$networkResponseSize = $this->params['networkResponseSize'] ?? 0;
$httpMethod = $this->params['httpMethod'] ?? '';
$httpRequest = $this->params['httpRequest'] ?? 0;
$functionId = $this->params['functionId'] ?? '';
$functionExecution = $this->params['functionExecution'] ?? 0;
$functionExecutionTime = $this->params['functionExecutionTime'] ?? 0;
$functionStatus = $this->params['functionStatus'] ?? '';
$tags = ",projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
// the global namespace is prepended to every key (optional)
$this->statsd->setNamespace($this->namespace);
if ($httpRequest >= 1) {
$this->statsd->increment('requests.all' . $tags . ',method=' . \strtolower($httpMethod));
}
if ($functionExecution >= 1) {
$this->statsd->increment('executions.all' . $tags . ',functionId=' . $functionId . ',functionStatus=' . $functionStatus);
$this->statsd->count('executions.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
}
$this->statsd->count('network.inbound' . $tags, $networkRequestSize);
$this->statsd->count('network.outbound' . $tags, $networkResponseSize);
$this->statsd->count('network.all' . $tags, $networkRequestSize + $networkResponseSize);
$dbMetrics = [
'databases.create',
'databases.read',
'databases.update',
'databases.delete',
'databases.collections.create',
'databases.collections.read',
'databases.collections.update',
'databases.collections.delete',
'databases.documents.create',
'databases.documents.read',
'databases.documents.update',
'databases.documents.delete',
];
foreach ($dbMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$tags = ",projectId={$projectId},collectionId=" . ($this->params['collectionId'] ?? '') . ",databaseId=" . ($this->params['databaseId'] ?? '');
$this->statsd->increment($metric . $tags);
}
}
$storageMertics = [
'storage.buckets.create',
'storage.buckets.read',
'storage.buckets.update',
'storage.buckets.delete',
'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);
}
$this->reset();
}
public function reset(): self
{
$this->params = [];
$this->namespace = 'appwrite.usage';
return $this;
}
}

View file

@ -1,358 +0,0 @@
<?php
namespace Appwrite\Stats;
use Utopia\Database\Database;
use Utopia\Database\Document;
use InfluxDB\Database as InfluxDatabase;
use DateTime;
class Usage
{
protected InfluxDatabase $influxDB;
protected Database $database;
protected $errorHandler;
private array $latestTime = [];
// all the mertics that we are collecting
protected array $metrics = [
'requests' => [
'table' => 'appwrite_usage_requests_all',
],
'network' => [
'table' => 'appwrite_usage_network_all',
],
'executions' => [
'table' => 'appwrite_usage_executions_all',
],
'databases.create' => [
'table' => 'appwrite_usage_databases_create',
],
'databases.read' => [
'table' => 'appwrite_usage_databases_read',
],
'databases.update' => [
'table' => 'appwrite_usage_databases_update',
],
'databases.delete' => [
'table' => 'appwrite_usage_databases_delete',
],
'databases.collections.create' => [
'table' => 'appwrite_usage_databases_collections_create',
],
'databases.collections.read' => [
'table' => 'appwrite_usage_databases_collections_read',
],
'databases.collections.update' => [
'table' => 'appwrite_usage_databases_collections_update',
],
'databases.collections.delete' => [
'table' => 'appwrite_usage_databases_collections_delete',
],
'databases.documents.create' => [
'table' => 'appwrite_usage_databases_documents_create',
],
'databases.documents.read' => [
'table' => 'appwrite_usage_databases_documents_read',
],
'databases.documents.update' => [
'table' => 'appwrite_usage_databases_documents_update',
],
'databases.documents.delete' => [
'table' => 'appwrite_usage_databases_documents_delete',
],
'databases.databaseId.collections.create' => [
'table' => 'appwrite_usage_databases_collections_create',
'groupBy' => ['databaseId'],
],
'databases.databaseId.collections.read' => [
'table' => 'appwrite_usage_databases_collections_read',
'groupBy' => ['databaseId'],
],
'databases.databaseId.collections.update' => [
'table' => 'appwrite_usage_databases_collections_update',
'groupBy' => ['databaseId'],
],
'databases.databaseId.collections.delete' => [
'table' => 'appwrite_usage_databases_collections_delete',
'groupBy' => ['databaseId'],
],
'databases.databaseId.documents.create' => [
'table' => 'appwrite_usage_databases_documents_create',
'groupBy' => ['databaseId'],
],
'databases.databaseId.documents.read' => [
'table' => 'appwrite_usage_databases_documents_read',
'groupBy' => ['databaseId'],
],
'database.databaseId.documents.update' => [
'table' => 'appwrite_usage_databases_documents_update',
'groupBy' => ['databaseId'],
],
'databases.databaseId.documents.delete' => [
'table' => 'appwrite_usage_databases_documents_delete',
'groupBy' => ['databaseId'],
],
'databases.databaseId.collections.collectionId.documents.create' => [
'table' => 'appwrite_usage_databases_documents_create',
'groupBy' => ['databaseId', 'collectionId'],
],
'databases.databaseId.collections.collectionId.documents.read' => [
'table' => 'appwrite_usage_databases_documents_read',
'groupBy' => ['databaseId', 'collectionId'],
],
'databases.databaseId.collections.collectionId.documents.update' => [
'table' => 'appwrite_usage_databases_documents_update',
'groupBy' => ['databaseId', 'collectionId'],
],
'databases.databaseId.collections.collectionId.documents.delete' => [
'table' => 'appwrite_usage_databases_documents_delete',
'groupBy' => ['databaseId', 'collectionId'],
],
'storage.buckets.create' => [
'table' => 'appwrite_usage_storage_buckets_create',
],
'storage.buckets.read' => [
'table' => 'appwrite_usage_storage_buckets_read',
],
'storage.buckets.update' => [
'table' => 'appwrite_usage_storage_buckets_update',
],
'storage.buckets.delete' => [
'table' => 'appwrite_usage_storage_buckets_delete',
],
'storage.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
],
'storage.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
],
'storage.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
],
'storage.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
],
'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',
],
],
];
protected array $periods = [
[
'key' => '30m',
'startTime' => '-24 hours',
],
[
'key' => '1d',
'startTime' => '-90 days',
],
];
public function __construct(Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
{
$this->database = $database;
$this->influxDB = $influxDB;
$this->errorHandler = $errorHandler;
}
/**
* Create or Update Mertic
* Create or update each metric in the stats collection for the given project
*
* @param string $projectId
* @param string $time
* @param string $period
* @param string $metric
* @param int $value
* @param int $type
*
* @return void
*/
private function createOrUpdateMetric(string $projectId, string $time, string $period, string $metric, int $value, int $type): void
{
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_console');
$project = $this->database->getDocument('projects', $projectId);
$this->database->setNamespace('_' . $project->getInternalId());
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => $type,
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$time = (new \DateTime($time))->getTimestamp(); //todo: What about this timestamp?
$this->latestTime[$metric][$period] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
}
/**
* Sync From InfluxDB
* Sync stats from influxDB to stats collection in the Appwrite database
*
* @param string $metric
* @param array $options
* @param array $period
*
* @return void
*/
private function syncFromInfluxDB(string $metric, array $options, array $period): void
{
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($this->latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $this->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']) ? '' : ', ' . implode(', ', array_map(fn($groupBy) => '"' . $groupBy . '" ', $options['groupBy'])); //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(fn ($filter, $value) => "\"{$filter}\"='{$value}'", array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$query = "SELECT sum(value) AS \"value\" ";
$query .= "FROM \"{$table}\" ";
$query .= "WHERE \"time\" > '{$start}' ";
$query .= "AND \"time\" < '{$end}' ";
$query .= "AND \"metric_type\"='counter' {$filters} ";
$query .= "GROUP BY time({$period['key']}), \"projectId\" {$groupBy} ";
$query .= "FILL(null)";
try {
$result = $this->influxDB->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId !== 'console') {
$metricUpdated = $metric;
if (!empty($groupBy)) {
foreach ($options['groupBy'] as $groupBy) {
$groupedBy = $point[$groupBy] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($groupBy, $groupedBy, $metricUpdated);
}
}
$time = $point['time']; //todo: check is this datetime format?
$value = (!empty($point['value'])) ? $point['value'] : 0;
$this->createOrUpdateMetric(
$projectId,
$time,
$period['key'],
$metricUpdated,
$value,
0
);
}
}
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_metric_{$metric}_influxdb");
} else {
throw $e;
}
}
}
/**
* Collect Stats
* Collect all the stats from Influd DB to Database
*
* @return void
*/
public function collect(): void
{
foreach ($this->metrics as $metric => $options) { //for each metrics
foreach ($this->periods as $period) { // aggregate data for each period
try {
$this->syncFromInfluxDB($metric, $options, $period);
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e);
} else {
throw $e;
}
}
}
}
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Appwrite\Usage;
abstract class Calculator
{
abstract public function collect(): void;
}

View file

@ -0,0 +1,229 @@
<?php
namespace Appwrite\Usage\Calculators;
use Utopia\Database\Database as UtopiaDatabase;
use Utopia\Database\Document;
use Utopia\Database\Query;
class Aggregator extends Database
{
protected function aggregateDatabaseMetrics(string $projectId): void
{
$this->database->setNamespace('_' . $projectId);
$databasesGeneralMetrics = [
'databases.$all.requests.create',
'databases.$all.requests.read',
'databases.$all.requests.update',
'databases.$all.requests.delete',
'collections.$all.requests.create',
'collections.$all.requests.read',
'collections.$all.requests.update',
'collections.$all.requests.delete',
'documents.$all.requests.create',
'documents.$all.requests.read',
'documents.$all.requests.update',
'documents.$all.requests.delete'
];
foreach ($databasesGeneralMetrics as $metric) {
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
$databasesDatabaseMetrics = [
'collections.databaseId.requests.create',
'collections.databaseId.requests.read',
'collections.databaseId.requests.update',
'collections.databaseId.requests.delete',
'documents.databaseId.requests.create',
'documents.databaseId.requests.read',
'documents.databaseId.requests.update',
'documents.databaseId.requests.delete',
];
$this->foreachDocument($projectId, 'databases', [], function (Document $database) use ($databasesDatabaseMetrics, $projectId) {
$databaseId = $database->getId();
foreach ($databasesDatabaseMetrics as $metric) {
$metric = str_replace('databaseId', $databaseId, $metric);
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
$databasesCollectionMetrics = [
'documents.' . $databaseId . '/collectionId.requests.create',
'documents.' . $databaseId . '/collectionId.requests.read',
'documents.' . $databaseId . '/collectionId.requests.update',
'documents.' . $databaseId . '/collectionId.requests.delete',
];
$this->foreachDocument($projectId, 'database_' . $database->getInternalId(), [], function (Document $collection) use ($databasesCollectionMetrics, $projectId) {
$collectionId = $collection->getId();
foreach ($databasesCollectionMetrics as $metric) {
$metric = str_replace('collectionId', $collectionId, $metric);
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
});
});
}
protected function aggregateStorageMetrics(string $projectId): void
{
$this->database->setNamespace('_' . $projectId);
$storageGeneralMetrics = [
'buckets.$all.requests.create',
'buckets.$all.requests.read',
'buckets.$all.requests.update',
'buckets.$all.requests.delete',
'files.$all.requests.create',
'files.$all.requests.read',
'files.$all.requests.update',
'files.$all.requests.delete',
];
foreach ($storageGeneralMetrics as $metric) {
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
$storageBucketMetrics = [
'files.bucketId.requests.create',
'files.bucketId.requests.read',
'files.bucketId.requests.update',
'files.bucketId.requests.delete',
];
$this->foreachDocument($projectId, 'buckets', [], function (Document $bucket) use ($storageBucketMetrics, $projectId) {
$bucketId = $bucket->getId();
foreach ($storageBucketMetrics as $metric) {
$metric = str_replace('bucketId', $bucketId, $metric);
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
});
}
protected function aggregateFunctionMetrics(string $projectId): void
{
$this->database->setNamespace('_' . $projectId);
$functionsGeneralMetrics = [
'project.$all.compute.total',
'project.$all.compute.time',
'executions.$all.compute.total',
'executions.$all.compute.success',
'executions.$all.compute.failure',
'executions.$all.compute.time',
'builds.$all.compute.total',
'builds.$all.compute.success',
'builds.$all.compute.failure',
'builds.$all.compute.time',
];
foreach ($functionsGeneralMetrics as $metric) {
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
$functionMetrics = [
'executions.functionId.compute.total',
'executions.functionId.compute.success',
'executions.functionId.compute.failure',
'executions.functionId.compute.time',
'builds.functionId.compute.total',
'builds.functionId.compute.success',
'builds.functionId.compute.failure',
'builds.functionId.compute.time',
];
$this->foreachDocument($projectId, 'functions', [], function (Document $function) use ($functionMetrics, $projectId) {
$functionId = $function->getId();
foreach ($functionMetrics as $metric) {
$metric = str_replace('functionId', $functionId, $metric);
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
});
}
protected function aggregateUsersMetrics(string $projectId): void
{
$metrics = [
'users.$all.requests.create',
'users.$all.requests.read',
'users.$all.requests.update',
'users.$all.requests.delete',
'sessions.$all.requests.create',
'sessions.$all.requests.delete'
];
foreach ($metrics as $metric) {
$this->aggregateDailyMetric($projectId, $metric);
$this->aggregateMonthlyMetric($projectId, $metric);
}
}
protected function aggregateGeneralMetrics(string $projectId): void
{
$this->aggregateDailyMetric($projectId, 'project.$all.network.requests');
$this->aggregateDailyMetric($projectId, 'project.$all.network.bandwidth');
$this->aggregateDailyMetric($projectId, 'project.$all.network.inbound');
$this->aggregateDailyMetric($projectId, 'project.$all.network.outbound');
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.requests');
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.bandwidth');
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.inbound');
$this->aggregateMonthlyMetric($projectId, 'project.$all.network.outbound');
}
protected function aggregateDailyMetric(string $projectId, string $metric): void
{
$beginOfDay = strtotime("today");
$endOfDay = strtotime("tomorrow", $beginOfDay) - 1;
$this->database->setNamespace('_' . $projectId);
$value = (int) $this->database->sum('stats', 'value', [
new Query('metric', Query::TYPE_EQUAL, [$metric]),
new Query('period', Query::TYPE_EQUAL, ['30m']),
new Query('time', Query::TYPE_GREATEREQUAL, [$beginOfDay]),
new Query('time', Query::TYPE_LESSEREQUAL, [$endOfDay]),
]);
$this->createOrUpdateMetric($projectId, $metric, '1d', $beginOfDay, $value);
}
protected function aggregateMonthlyMetric(string $projectId, string $metric): void
{
$beginOfMonth = strtotime("first day of the month");
$endOfMonth = strtotime("last day of the month");
$this->database->setNamespace('_' . $projectId);
$value = (int) $this->database->sum('stats', 'value', [
new Query('metric', Query::TYPE_EQUAL, [$metric]),
new Query('period', Query::TYPE_EQUAL, ['1d']),
new Query('time', Query::TYPE_GREATEREQUAL, [$beginOfMonth]),
new Query('time', Query::TYPE_LESSEREQUAL, [$endOfMonth]),
]);
$this->createOrUpdateMetric($projectId, $metric, '1mo', $beginOfMonth, $value);
}
/**
* Collect Stats
* Collect all database related stats
*
* @return void
*/
public function collect(): void
{
$this->foreachDocument('console', 'projects', [], function (Document $project) {
$projectId = $project->getInternalId();
// Aggregate new metrics from already collected usage metrics
// for lower time period (1day and 1 month metric from 30 minute metrics)
$this->aggregateGeneralMetrics($projectId);
$this->aggregateFunctionMetrics($projectId);
$this->aggregateDatabaseMetrics($projectId);
$this->aggregateStorageMetrics($projectId);
$this->aggregateUsersMetrics($projectId);
});
}
}

View file

@ -1,32 +1,48 @@
<?php
namespace Appwrite\Stats;
namespace Appwrite\Usage\Calculators;
use Exception;
use Utopia\Database\Database;
use Appwrite\Usage\Calculator;
use Utopia\Database\Database as UtopiaDatabase;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
class UsageDB extends Usage
class Database extends Calculator
{
public function __construct(Database $database, callable $errorHandler = null)
protected array $periods = [
[
'key' => '30m',
'multiplier' => 1800,
],
[
'key' => '1d',
'multiplier' => 86400,
],
];
public function __construct(UtopiaDatabase $database, callable $errorHandler = null)
{
$this->database = $database;
$this->errorHandler = $errorHandler;
}
/**
* Create or Update Mertic
* Create or update each metric in the stats collection for the given project
* Create Per Period Metric
*
* Create given metric for each defined period
*
* @param string $projectId
* @param string $metric
* @param int $value
*
* @param bool $monthly
* @return void
* @throws Exception
* @throws Authorization
* @throws Structure
*/
private function createOrUpdateMetric(string $projectId, string $metric, int $value): void
protected function createPerPeriodMetric(string $projectId, string $metric, int $value, bool $monthly = false): void
{
foreach ($this->periods as $options) {
$period = $options['key'];
@ -39,40 +55,66 @@ class UsageDB extends Usage
} else {
throw new Exception("Period type not found", 500);
}
$this->createOrUpdateMetric($projectId, $metric, $period, $time, $value);
}
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_' . $projectId);
// Required for billing
if ($monthly) {
$time = strtotime("first day of the month");
$this->createOrUpdateMetric($projectId, $metric, '1mo', $time, $value);
}
}
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => 1,
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
/**
* Create or Update Metric
*
* Create or update each metric in the stats collection for the given project
*
* @param string $projectId
* @param string $metric
* @param string $period
* @param string $time
* @param int $value
*
* @return void
* @throws Authorization
* @throws Structure
*/
protected function createOrUpdateMetric(string $projectId, string $metric, string $period, string $time, int $value): void
{
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_' . $projectId);
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => 2, // these are cumulative metrics
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
} catch (\Exception$e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
}
/**
* Foreach Document
*
* Call provided callback for each document in the collection
*
* @param string $projectId
@ -81,8 +123,9 @@ class UsageDB extends Usage
* @param callable $callback
*
* @return void
* @throws Exception
*/
private function foreachDocument(string $projectId, string $collection, array $queries, callable $callback): void
protected function foreachDocument(string $projectId, string $collection, array $queries, callable $callback): void
{
$limit = 50;
$results = [];
@ -122,23 +165,28 @@ class UsageDB extends Usage
/**
* Sum
* Calculate sum of a attribute of documents in collection
*
* Calculate sum of an attribute of documents in collection
*
* @param string $projectId
* @param string $collection
* @param string $attribute
* @param string $metric
*
* @param string|null $metric
* @param int $multiplier
* @return int
* @throws Exception
*/
private function sum(string $projectId, string $collection, string $attribute, string $metric): int
private function sum(string $projectId, string $collection, string $attribute, string $metric = null, int $multiplier = 1): int
{
$this->database->setNamespace('_' . $projectId);
try {
$sum = (int) $this->database->sum($collection, $attribute);
$this->createOrUpdateMetric($projectId, $metric, $sum);
$sum = $this->database->sum($collection, $attribute);
$sum = (int) ($sum * $multiplier);
if (!is_null($metric)) {
$this->createPerPeriodMetric($projectId, $metric, $sum);
}
return $sum;
} catch (Exception $e) {
if (is_callable($this->errorHandler)) {
@ -147,26 +195,30 @@ class UsageDB extends Usage
throw $e;
}
}
return 0;
}
/**
* Count
*
* Count number of documents in collection
*
* @param string $projectId
* @param string $collection
* @param string $metric
* @param ?string $metric
*
* @return int
* @throws Exception
*/
private function count(string $projectId, string $collection, string $metric): int
private function count(string $projectId, string $collection, ?string $metric = null): int
{
$this->database->setNamespace('_' . $projectId);
try {
$count = $this->database->count($collection);
$this->createOrUpdateMetric($projectId, $metric, $count);
if (!is_null($metric)) {
$this->createPerPeriodMetric($projectId, (string) $metric, $count);
}
return $count;
} catch (Exception $e) {
if (is_callable($this->errorHandler)) {
@ -175,113 +227,124 @@ class UsageDB extends Usage
throw $e;
}
}
return 0;
}
/**
* Deployments Total
*
* Total sum of storage used by deployments
*
* @param string $projectId
*
* @return int
* @throws Exception
*/
private function deploymentsTotal(string $projectId): int
{
return $this->sum($projectId, 'deployments', 'size', 'stroage.deployments.total');
return $this->sum($projectId, 'deployments', 'size', 'deployments.$all.storage.size');
}
/**
* Users Stats
*
* Metric: users.count
*
* @param string $projectId
*
* @return void
* @throws Exception
*/
private function usersStats(string $projectId): void
{
$this->count($projectId, 'users', 'users.count');
$this->count($projectId, 'users', 'users.$all.count.total');
}
/**
* Storage Stats
* Metrics: storage.total, storage.files.total, storage.buckets.{bucketId}.files.total,
* storage.buckets.count, storage.files.count, storage.buckets.{bucketId}.files.count
*
* Metrics: buckets.$all.count.total, files.$all.count.total, files.bucketId,count.total,
* files.$all.storage.size, files.bucketId.storage.size, project.$all.storage.size
*
* @param string $projectId
*
* @return void
* @throws Authorization
* @throws Structure
*/
private function storageStats(string $projectId): void
{
$deploymentsTotal = $this->deploymentsTotal($projectId);
$projectFilesTotal = 0;
$projectFilesCount = 0;
$metric = 'storage.buckets.count';
$metric = 'buckets.$all.count.total';
$this->count($projectId, 'buckets', $metric);
$this->foreachDocument($projectId, 'buckets', [], function ($bucket) use (&$projectFilesCount, &$projectFilesTotal, $projectId,) {
$metric = "storage.buckets.{$bucket->getId()}.files.count";
$metric = "files.{$bucket->getId()}.count.total";
$count = $this->count($projectId, 'bucket_' . $bucket->getInternalId(), $metric);
$projectFilesCount += $count;
$metric = "storage.buckets.{$bucket->getId()}.files.total";
$metric = "files.{$bucket->getId()}.storage.size";
$sum = $this->sum($projectId, 'bucket_' . $bucket->getInternalId(), 'sizeOriginal', $metric);
$projectFilesTotal += $sum;
});
$this->createOrUpdateMetric($projectId, 'storage.files.count', $projectFilesCount);
$this->createOrUpdateMetric($projectId, 'storage.files.total', $projectFilesTotal);
$this->createPerPeriodMetric($projectId, 'files.$all.count.total', $projectFilesCount);
$this->createPerPeriodMetric($projectId, 'files.$all.storage.size', $projectFilesTotal);
$this->createOrUpdateMetric($projectId, 'storage.total', $projectFilesTotal + $deploymentsTotal);
$deploymentsTotal = $this->deploymentsTotal($projectId);
$this->createPerPeriodMetric($projectId, 'project.$all.storage.size', $projectFilesTotal + $deploymentsTotal);
}
/**
* Database Stats
*
* Collect all database stats
* Metrics: database.collections.count, database.collections.{collectionId}.documents.count,
* database.documents.count
* Metrics: databases.$all.count.total, collections.$all.count.total, collections.databaseId.count.total,
* documents.$all.count.all, documents.databaseId.count.total, documents.databaseId/collectionId.count.total
*
* @param string $projectId
*
* @return void
* @throws Authorization
* @throws Structure
*/
private function databaseStats(string $projectId): void
{
$projectDocumentsCount = 0;
$projectCollectionsCount = 0;
$this->count($projectId, 'databases', 'databases.count');
$this->count($projectId, 'databases', 'databases.$all.count.total');
$this->foreachDocument($projectId, 'databases', [], function ($database) use (&$projectDocumentsCount, &$projectCollectionsCount, $projectId) {
$metric = "databases.{$database->getId()}.collections.count";
$metric = "collections.{$database->getId()}.count.total";
$count = $this->count($projectId, 'database_' . $database->getInternalId(), $metric);
$projectCollectionsCount += $count;
$databaseDocumentsCount = 0;
$this->foreachDocument($projectId, 'database_' . $database->getInternalId(), [], function ($collection) use (&$projectDocumentsCount, &$databaseDocumentsCount, $projectId, $database) {
$metric = "databases.{$database->getId()}.collections.{$collection->getId()}.documents.count";
$metric = "documents.{$database->getId()}/{$collection->getId()}.count.total";
$count = $this->count($projectId, 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $metric);
$projectDocumentsCount += $count;
$databaseDocumentsCount += $count;
});
$this->createOrUpdateMetric($projectId, "databases.{$database->getId()}.documents.count", $databaseDocumentsCount);
$this->createPerPeriodMetric($projectId, "documents.{$database->getId()}.count.total", $databaseDocumentsCount);
});
$this->createOrUpdateMetric($projectId, 'databases.collections.count', $projectCollectionsCount);
$this->createOrUpdateMetric($projectId, 'databases.documents.count', $projectDocumentsCount);
$this->createPerPeriodMetric($projectId, 'collections.$all.count.total', $projectCollectionsCount);
$this->createPerPeriodMetric($projectId, 'documents.$all.count.total', $projectDocumentsCount);
}
/**
* Collect Stats
*
* Collect all database related stats
*
* @return void
* @throws Exception
*/
public function collect(): void
{

View file

@ -0,0 +1,432 @@
<?php
namespace Appwrite\Usage\Calculators;
use Appwrite\Usage\Calculator;
use Utopia\Database\Database;
use Utopia\Database\Document;
use InfluxDB\Database as InfluxDatabase;
use DateTime;
class TimeSeries extends Calculator
{
protected InfluxDatabase $influxDB;
protected Database $database;
protected $errorHandler;
private array $latestTime = [];
// all the mertics that we are collecting
protected array $metrics = [
'project.$all.network.requests' => [
'table' => 'appwrite_usage_project_{scope}_network_requests',
],
'project.$all.network.bandwidth' => [
'table' => 'appwrite_usage_project_{scope}_network_bandwidth',
],
'project.$all.network.inbound' => [
'table' => 'appwrite_usage_project_{scope}_network_inbound',
],
'project.$all.network.outbound' => [
'table' => 'appwrite_usage_project_{scope}_network_outbound',
],
/* Users service metrics */
'users.$all.requests.create' => [
'table' => 'appwrite_usage_users_{scope}_requests_create',
],
'users.$all.requests.read' => [
'table' => 'appwrite_usage_users_{scope}_requests_read',
],
'users.$all.requests.update' => [
'table' => 'appwrite_usage_users_{scope}_requests_update',
],
'users.$all.requests.delete' => [
'table' => 'appwrite_usage_users_{scope}_requests_delete',
],
'databases.$all.requests.create' => [
'table' => 'appwrite_usage_databases_{scope}_requests_create',
],
'databases.$all.requests.read' => [
'table' => 'appwrite_usage_databases_{scope}_requests_read',
],
'databases.$all.requests.update' => [
'table' => 'appwrite_usage_databases_{scope}_requests_update',
],
'databases.$all.requests.delete' => [
'table' => 'appwrite_usage_databases_{scope}_requests_delete',
],
'collections.$all.requests.create' => [
'table' => 'appwrite_usage_collections_{scope}_requests_create',
],
'collections.$all.requests.read' => [
'table' => 'appwrite_usage_collections_{scope}_requests_read',
],
'collections.$all.requests.update' => [
'table' => 'appwrite_usage_collections_{scope}_requests_update',
],
'collections.$all.requests.delete' => [
'table' => 'appwrite_usage_collections_{scope}_requests_delete',
],
'documents.$all.requests.create' => [
'table' => 'appwrite_usage_documents_{scope}_requests_create',
],
'documents.$all.requests.read' => [
'table' => 'appwrite_usage_documents_{scope}_requests_read',
],
'documents.$all.requests.update' => [
'table' => 'appwrite_usage_documents_{scope}_requests_update',
],
'documents.$all.requests.delete' => [
'table' => 'appwrite_usage_documents_{scope}_requests_delete',
],
'collections.databaseId.requests.create' => [
'table' => 'appwrite_usage_collections_{scope}_requests_create',
'groupBy' => ['databaseId'],
],
'collections.databaseId.requests.read' => [
'table' => 'appwrite_usage_collections_{scope}_requests_read',
'groupBy' => ['databaseId'],
],
'collections.databaseId.requests.update' => [
'table' => 'appwrite_usage_collections_{scope}_requests_update',
'groupBy' => ['databaseId'],
],
'collections.databaseId.requests.delete' => [
'table' => 'appwrite_usage_collections_{scope}_requests_delete',
'groupBy' => ['databaseId'],
],
'documents.databaseId.requests.create' => [
'table' => 'appwrite_usage_documents_{scope}_requests_create',
'groupBy' => ['databaseId'],
],
'documents.databaseId.requests.read' => [
'table' => 'appwrite_usage_documents_{scope}_requests_read',
'groupBy' => ['databaseId'],
],
'documents.databaseId.requests.update' => [
'table' => 'appwrite_usage_documents_{scope}_requests_update',
'groupBy' => ['databaseId'],
],
'documents.databaseId.requests.delete' => [
'table' => 'appwrite_usage_documents_{scope}_requests_delete',
'groupBy' => ['databaseId'],
],
'documents.databaseId/collectionId.requests.create' => [
'table' => 'appwrite_usage_documents_{scope}_requests_create',
'groupBy' => ['databaseId', 'collectionId'],
],
'documents.databaseId/collectionId.requests.read' => [
'table' => 'appwrite_usage_documents_{scope}_requests_read',
'groupBy' => ['databaseId', 'collectionId'],
],
'documents.databaseId/collectionId.requests.update' => [
'table' => 'appwrite_usage_documents_{scope}_requests_update',
'groupBy' => ['databaseId', 'collectionId'],
],
'documents.databaseId/collectionId.requests.delete' => [
'table' => 'appwrite_usage_documents_{scope}_requests_delete',
'groupBy' => ['databaseId', 'collectionId'],
],
'buckets.$all.requests.create' => [
'table' => 'appwrite_usage_buckets_{scope}_requests_create',
],
'buckets.$all.requests.read' => [
'table' => 'appwrite_usage_buckets_{scope}_requests_read',
],
'buckets.$all.requests.update' => [
'table' => 'appwrite_usage_buckets_{scope}_requests_update',
],
'buckets.$all.requests.delete' => [
'table' => 'appwrite_usage_buckets_{scope}_requests_delete',
],
'files.$all.requests.create' => [
'table' => 'appwrite_usage_files_{scope}_requests_create',
],
'files.$all.requests.read' => [
'table' => 'appwrite_usage_files_{scope}_requests_read',
],
'files.$all.requests.update' => [
'table' => 'appwrite_usage_files_{scope}_requests_update',
],
'files.$all.requests.delete' => [
'table' => 'appwrite_usage_files_{scope}_requests_delete',
],
'files.bucketId.requests.create' => [
'table' => 'appwrite_usage_files_{scope}_requests_create',
'groupBy' => ['bucketId'],
],
'files.bucketId.requests.read' => [
'table' => 'appwrite_usage_files_{scope}_requests_read',
'groupBy' => ['bucketId'],
],
'files.bucketId.requests.update' => [
'table' => 'appwrite_usage_files_{scope}_requests_update',
'groupBy' => ['bucketId'],
],
'files.bucketId.requests.delete' => [
'table' => 'appwrite_usage_files_{scope}_requests_delete',
'groupBy' => ['bucketId'],
],
'sessions.$all.requests.create' => [
'table' => 'appwrite_usage_sessions__{scope}_requests_create',
],
'sessions.provider.requests.create' => [
'table' => 'appwrite_usage_sessions_{scope}_requests_create',
'groupBy' => ['provider'],
],
'sessions.$all.requests.delete' => [
'table' => 'appwrite_usage_sessions_{scope}_requests_delete',
],
'executions.$all.compute.total' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
],
'builds.$all.compute.time' => [
'table' => 'appwrite_usage_executions_{scope}_compute_time',
],
'executions.$all.compute.time' => [
'table' => 'appwrite_usage_executions_{scope}_compute_time',
],
'builds.$all.compute.total' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
],
'executions.$all.compute.failure' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
'filters' => [
'functionStatus' => 'failed',
],
],
'builds.$all.compute.failure' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
'filters' => [
'functionStatus' => 'failed',
],
],
'executions.$all.compute.success' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
'filters' => [
'functionStatus' => 'success',
],
],
'builds.$all.compute.success' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
'filters' => [
'functionStatus' => 'success',
],
],
'executions.functionId.compute.total' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
'groupBy' => ['functionId'],
],
'builds.functionId.compute.total' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
'groupBy' => ['functionId'],
],
'executions.functionId.compute.time' => [
'table' => 'appwrite_usage_executions_{scope}_compute_time',
'groupBy' => ['functionId'],
],
'builds.functionId.compute.time' => [
'table' => 'appwrite_usage_builds_{scope}_compute_time',
'groupBy' => ['functionId'],
],
'executions.functionId.compute.failure' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
'groupBy' => ['functionId'],
'filters' => [
'functionStatus' => 'failed',
],
],
'builds.functionId.compute.failure' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
'groupBy' => ['functionId'],
'filters' => [
'functionBuildStatus' => 'failed',
],
],
'executions.functionId.compute.success' => [
'table' => 'appwrite_usage_executions_{scope}_compute',
'groupBy' => ['functionId'],
'filters' => [
'functionStatus' => 'success',
],
],
'builds.functionId.compute.success' => [
'table' => 'appwrite_usage_builds_{scope}_compute',
'groupBy' => ['functionId'],
'filters' => [
'functionBuildStatus' => 'success',
],
],
'project.$all.compute.time' => [ // Built time + execution time
'table' => 'appwrite_usage_project_{scope}_compute_time',
'groupBy' => ['functionId'],
],
];
protected array $period = [
'key' => '30m',
'startTime' => '-24 hours',
];
public function __construct(Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
{
$this->database = $database;
$this->influxDB = $influxDB;
$this->errorHandler = $errorHandler;
}
/**
* Create or Update Mertic
* Create or update each metric in the stats collection for the given project
*
* @param string $projectId
* @param int $time
* @param string $period
* @param string $metric
* @param int $value
* @param int $type
*
* @return void
*/
private function createOrUpdateMetric(string $projectId, int $time, string $period, string $metric, int $value, int $type): void
{
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_console');
$project = $this->database->getDocument('projects', $projectId);
$this->database->setNamespace('_' . $project->getInternalId());
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => $type,
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$this->latestTime[$metric][$period] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
}
/**
* Sync From InfluxDB
* Sync stats from influxDB to stats collection in the Appwrite database
*
* @param string $metric
* @param array $options
* @param array $period
*
* @return void
*/
private function syncFromInfluxDB(string $metric, array $options, array $period): void
{
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($this->latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $this->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']) ? '' : ', ' . implode(', ', array_map(fn($groupBy) => '"' . $groupBy . '" ', $options['groupBy'])); //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(fn ($filter, $value) => "\"{$filter}\"='{$value}'", array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$query = "SELECT sum(value) AS \"value\" ";
$query .= "FROM \"{$table}\" ";
$query .= "WHERE \"time\" > '{$start}' ";
$query .= "AND \"time\" < '{$end}' ";
$query .= "AND \"metric_type\"='counter' {$filters} ";
$query .= "GROUP BY time({$period['key']}), \"projectId\" {$groupBy} ";
$query .= "FILL(null)";
try {
$result = $this->influxDB->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId !== 'console') {
$metricUpdated = $metric;
if (!empty($groupBy)) {
foreach ($options['groupBy'] as $groupBy) {
$groupedBy = $point[$groupBy] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($groupBy, $groupedBy, $metricUpdated);
}
}
$time = \strtotime($point['time']);
$value = (!empty($point['value'])) ? $point['value'] : 0;
$this->createOrUpdateMetric(
$projectId,
$time,
$period['key'],
$metricUpdated,
$value,
0
);
}
}
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_metric_{$metric}_influxdb");
} else {
throw $e;
}
}
}
/**
* Collect Stats
* Collect all the stats from Influd DB to Database
*
* @return void
*/
public function collect(): void
{
foreach ($this->metrics as $metric => $options) { //for each metrics
try {
$this->syncFromInfluxDB($metric, $options, $this->period);
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e);
} else {
throw $e;
}
}
}
}
}

View file

@ -0,0 +1,204 @@
<?php
namespace Appwrite\Usage;
use Utopia\App;
class Stats
{
/**
* @var array
*/
protected $params = [];
/**
* @var mixed
*/
protected $statsd;
/**
* @var string
*/
protected $namespace = 'appwrite.usage';
/**
* Event constructor.
*
* @param mixed $statsd
*/
public function __construct($statsd)
{
$this->statsd = $statsd;
}
/**
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setParam(string $key, $value): self
{
$this->params[$key] = $value;
return $this;
}
/**
* @param string $key
*
* @return mixed|null
*/
public function getParam(string $key)
{
return (isset($this->params[$key])) ? $this->params[$key] : null;
}
/**
* @param string $namespace
*
* @return $this
*/
public function setNamespace(string $namespace): self
{
$this->namespace = $namespace;
return $this;
}
/**
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Submit data to StatsD.
*/
public function submit(): void
{
$projectId = $this->params['projectId'] ?? '';
$tags = ",projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
// the global namespace is prepended to every key (optional)
$this->statsd->setNamespace($this->namespace);
$httpRequest = $this->params['project.{scope}.network.requests'] ?? 0;
$httpMethod = $this->params['httpMethod'] ?? '';
if ($httpRequest >= 1) {
$this->statsd->increment('project.{scope}.network.requests' . $tags . ',method=' . \strtolower($httpMethod));
}
$inbound = $this->params['networkRequestSize'] ?? 0;
$outbound = $this->params['networkResponseSize'] ?? 0;
$this->statsd->count('project.{scope}.network.inbound' . $tags, $inbound);
$this->statsd->count('project.{scope}.network.outbound' . $tags, $outbound);
$this->statsd->count('project.{scope}.network.bandwidth' . $tags, $inbound + $outbound);
$usersMetrics = [
'users.{scope}.requests.create',
'users.{scope}.requests.read',
'users.{scope}.requests.update',
'users.{scope}.requests.delete',
];
foreach ($usersMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$this->statsd->increment($metric . $tags);
}
}
$dbMetrics = [
'databases.{scope}.requests.create',
'databases.{scope}.requests.read',
'databases.{scope}.requests.update',
'databases.{scope}.requests.delete',
'collections.{scope}.requests.create',
'collections.{scope}.requests.read',
'collections.{scope}.requests.update',
'collections.{scope}.requests.delete',
'documents.{scope}.requests.create',
'documents.{scope}.requests.read',
'documents.{scope}.requests.update',
'documents.{scope}.requests.delete',
];
foreach ($dbMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$dbTags = $tags . ",collectionId=" . ($this->params['collectionId'] ?? '') . ",databaseId=" . ($this->params['databaseId'] ?? '');
$this->statsd->increment($metric . $dbTags);
}
}
$storageMertics = [
'buckets.{scope}.requests.create',
'buckets.{scope}.requests.read',
'buckets.{scope}.requests.update',
'buckets.{scope}.requests.delete',
'files.{scope}.requests.create',
'files.{scope}.requests.read',
'files.{scope}.requests.update',
'files.{scope}.requests.delete',
];
foreach ($storageMertics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$storageTags = $tags . ",bucketId=" . ($this->params['bucketId'] ?? '');
$this->statsd->increment($metric . $storageTags);
}
}
$sessionsMetrics = [
'sessions.{scope}.requests.create',
'sessions.{scope}.requests.update',
'sessions.{scope}.requests.delete',
];
foreach ($sessionsMetrics as $metric) {
$value = $this->params[$metric] ?? 0;
if ($value >= 1) {
$sessionTags = $tags . ",provider=" . ($this->params['provider'] ?? '');
$this->statsd->count($metric . $sessionTags, $value);
}
}
$functionId = $this->params['functionId'] ?? '';
$functionExecution = $this->params['executions.{scope}.compute'] ?? 0;
$functionExecutionTime = ($this->params['executionTime'] ?? 0) * 1000; // ms
$functionExecutionStatus = $this->params['executionStatus'] ?? '';
$functionBuild = $this->params['builds.{scope}.compute'] ?? 0;
$functionBuildTime = ($this->params['buildTime'] ?? 0) * 1000; // ms
$functionBuildStatus = $this->params['buildStatus'] ?? '';
$functionCompute = $functionExecutionTime + $functionBuildTime;
if ($functionExecution >= 1) {
$this->statsd->increment('executions.{scope}.compute' . $tags . ',functionId=' . $functionId . ',functionStatus=' . $functionExecutionStatus);
if ($functionExecutionTime > 0) {
$this->statsd->count('executions.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
}
}
if ($functionBuild >= 1) {
$this->statsd->increment('builds.{scope}.compute' . $tags . ',functionId=' . $functionId . ',functionBuildStatus=' . $functionBuildStatus);
$this->statsd->count('builds.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionBuildTime);
}
if ($functionBuild + $functionExecution >= 1) {
$this->statsd->count('project.{scope}.compute.time' . $tags . ',functionId=' . $functionId, $functionCompute);
}
$this->reset();
}
public function reset(): self
{
$this->params = [];
$this->namespace = 'appwrite.usage';
return $this;
}
}

View file

@ -0,0 +1,153 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class IndexedQueries extends Queries
{
/**
* @var Document[]
*/
protected $attributes = [];
/**
* @var Document[]
*/
protected $indexes = [];
/**
* Expression constructor
*
* This Queries Validator filters indexes for only available indexes
*
* @param Document[] $attributes
* @param Document[] $indexes
* @param Base ...$validators
* @param bool $strict
*/
public function __construct($attributes = [], $indexes = [], Base ...$validators)
{
$this->attributes = $attributes;
$this->indexes[] = new Document([
'type' => Database::INDEX_UNIQUE,
'attributes' => ['$id']
]);
$this->indexes[] = new Document([
'type' => Database::INDEX_KEY,
'attributes' => ['$createdAt']
]);
$this->indexes[] = new Document([
'type' => Database::INDEX_KEY,
'attributes' => ['$updatedAt']
]);
foreach ($indexes ?? [] as $index) {
$this->indexes[] = $index;
}
parent::__construct(...$validators);
}
/**
* Check if indexed array $indexes matches $queries
*
* @param array $indexes
* @param array $queries
*
* @return bool
*/
protected function arrayMatch(array $indexes, array $queries): bool
{
// Check the count of indexes first for performance
if (count($queries) !== count($indexes)) {
return false;
}
// Sort them for comparison, the order is not important here anymore.
sort($indexes, SORT_STRING);
sort($queries, SORT_STRING);
// Only matching arrays will have equal diffs in both directions
if (array_diff_assoc($indexes, $queries) !== array_diff_assoc($queries, $indexes)) {
return false;
}
return true;
}
/**
* Is valid.
*
* Returns false if:
* 1. any query in $value is invalid based on $validator
* 2. there is no index with an exact match of the filters
* 3. there is no index with an exact match of the order attributes
*
* Otherwise, returns true.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (!parent::isValid($value)) {
return false;
}
$queries = [];
foreach ($value as $query) {
if (!$query instanceof Query) {
$query = Query::parse($query);
}
$queries[] = $query;
}
$grouped = Query::groupByType($queries);
/** @var Query[] */ $filters = $grouped['filters'];
/** @var string[] */ $orderAttributes = $grouped['orderAttributes'];
// Check filter queries for exact index match
if (count($filters) > 0) {
$filtersByAttribute = [];
foreach ($filters as $filter) {
$filtersByAttribute[$filter->getAttribute()] = $filter->getMethod();
}
$found = null;
foreach ($this->indexes as $index) {
if ($this->arrayMatch($index->getAttribute('attributes'), array_keys($filtersByAttribute))) {
$found = $index;
}
}
if (!$found) {
$this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
// search method requires fulltext index
if (in_array(Query::TYPE_SEARCH, array_values($filtersByAttribute)) && $found['type'] !== Database::INDEX_FULLTEXT) {
$this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
}
// Check order attributes for exact index match
$validator = new OrderAttributes($this->attributes, $this->indexes, true);
if (count($orderAttributes) > 0 && !$validator->isValid($orderAttributes)) {
$this->message = $validator->getDescription();
return false;
}
return true;
}
}

View file

@ -2,28 +2,139 @@
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Document;
use Utopia\Database\Validator\Queries as ValidatorQueries;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Validator;
use Utopia\Database\Query;
class Queries extends ValidatorQueries
class Queries extends Validator
{
/**
* Expression constructor
*
* This Queries Validator that filters indexes for only available indexes
*
* @param QueryValidator $validator
* @param Document[] $attributes
* @param Document[] $indexes
* @param bool $strict
* @var string
*/
public function __construct($validator, $attributes = [], $indexes = [], $strict = true)
{
// Remove failed/stuck/processing indexes
$availableIndexes = \array_filter($indexes, function ($index) {
return $index->getAttribute('status') === 'available';
});
protected $message = 'Invalid queries';
parent::__construct($validator, $attributes, $availableIndexes, $strict);
/**
* @var Base[]
*/
protected $validators;
/**
* Queries constructor
*
* @param Base ...$validators a list of validators
*/
public function __construct(Base ...$validators)
{
$this->validators = $validators;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns false if:
* 1. any query in $value is invalid based on $validator
*
* Otherwise, returns true.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
foreach ($value as $query) {
if (!$query instanceof Query) {
try {
$query = Query::parse($query);
} catch (\Throwable $th) {
$this->message = 'Invalid query: ${query}';
return false;
}
}
$method = $query->getMethod();
$methodType = '';
switch ($method) {
case Query::TYPE_LIMIT:
$methodType = Base::METHOD_TYPE_LIMIT;
break;
case Query::TYPE_OFFSET:
$methodType = Base::METHOD_TYPE_OFFSET;
break;
case Query::TYPE_CURSORAFTER:
case Query::TYPE_CURSORBEFORE:
$methodType = Base::METHOD_TYPE_CURSOR;
break;
case Query::TYPE_ORDERASC:
case Query::TYPE_ORDERDESC:
$methodType = Base::METHOD_TYPE_ORDER;
break;
case Query::TYPE_EQUAL:
case Query::TYPE_NOTEQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSEREQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
$methodType = Base::METHOD_TYPE_FILTER;
break;
default:
break;
}
$methodIsValid = false;
foreach ($this->validators as $validator) {
if ($validator->getMethodType() !== $methodType) {
continue;
}
if (!$validator->isValid($query)) {
$this->message = 'Query not valid: ' . $validator->getDescription();
return false;
}
$methodIsValid = true;
}
if (!$methodIsValid) {
$this->message = 'Query method not valid: ' . $method;
return false;
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return true;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
class Base extends Queries
{
/**
* Expression constructor
*
* @param string $collection
* @param string[] $allowedAttributes
*/
public function __construct(string $collection, array $allowedAttributes)
{
$collection = Config::getParam('collections', [])[$collection];
// array for constant lookup time
$allowedAttributesLookup = [];
foreach ($allowedAttributes as $attribute) {
$allowedAttributesLookup[$attribute] = true;
}
$attributes = [];
foreach ($collection['attributes'] as $attribute) {
$key = $attribute['$id'];
if (!isset($allowedAttributesLookup[$key])) {
continue;
}
$attributes[] = new Document([
'key' => $key,
'type' => $attribute['type'],
'array' => $attribute['array'],
]);
}
$attributes[] = new Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Limit(),
new Offset(),
new Cursor(),
new Filter($attributes),
new Order($attributes),
];
parent::__construct(...$validators);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Buckets extends Base
{
public const ALLOWED_ATTRIBUTES = [
'enabled',
'name',
'fileSecurity',
'maximumFileSize',
'encryption',
'antivirus'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('buckets', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Collections extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'enabled',
'documentSecurity'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('collections', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Databases extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('databases', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Deployments extends Base
{
public const ALLOWED_ATTRIBUTES = [
'entrypoint',
'size',
'buildId',
'activate',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('deployments', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Utopia\Database\Database;
use Utopia\Database\Document;
class Documents extends IndexedQueries
{
/**
* Expression constructor
*
* @param Document[] $attributes
* @param Document[] $indexes
*/
public function __construct(array $attributes, array $indexes)
{
$attributes[] = new Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Limit(),
new Offset(),
new Cursor(),
new Filter($attributes),
new Order($attributes),
];
parent::__construct($attributes, $indexes, ...$validators);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Executions extends Base
{
public const ALLOWED_ATTRIBUTES = [
'trigger',
'status',
'statusCode',
'time'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('executions', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Files extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'signature',
'mimeType',
'sizeOriginal',
'chunksTotal',
'chunksUploaded'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('files', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Functions extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'status',
'runtime',
'deployment',
'schedule',
'scheduleNext',
'schedulePrevious',
'timeout'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('functions', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Memberships extends Base
{
public const ALLOWED_ATTRIBUTES = [
'userId',
'teamId',
'invited',
'joined',
'confirm'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('memberships', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Teams extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'total'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('teams', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
class Users extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'email',
'phone',
'status',
'passwordUpdate',
'registration',
'emailVerification',
'phoneVerification'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('users', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Validator;
use Utopia\Database\Query;
abstract class Base extends Validator
{
public const METHOD_TYPE_LIMIT = 'limit';
public const METHOD_TYPE_OFFSET = 'offset';
public const METHOD_TYPE_CURSOR = 'cursor';
public const METHOD_TYPE_ORDER = 'order';
public const METHOD_TYPE_FILTER = 'filter';
/**
* @var string
*/
protected $message = 'Invalid query';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
/**
* Is valid.
*
* @param Query $value
*
* @return bool
*/
abstract public function isValid($query): bool;
/**
* Returns what type of query this Validator is for
*/
abstract public function getMethodType(): string;
}

View file

@ -0,0 +1,44 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
class Cursor extends Base
{
/**
* Is valid.
*
* Returns true if method is cursorBefore or cursorAfter and value is not null
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method === Query::TYPE_CURSORAFTER || $method === Query::TYPE_CURSORBEFORE) {
$cursor = $query->getValue();
$validator = new UID();
if ($validator->isValid($cursor)) {
return true;
}
$this->message = 'Invalid cursor: ' . $validator->getDescription();
return false;
}
return false;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_CURSOR;
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Database;
use Utopia\Database\Query;
class Filter extends Base
{
/**
* @var string
*/
protected $message = 'Invalid query';
/**
* @var array
*/
protected $schema = [];
/**
* Query constructor
*
* @param int $maxValuesCount
*/
public function __construct(array $attributes = [], int $maxValuesCount = 100)
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
$this->maxValuesCount = $maxValuesCount;
}
protected function isValidAttribute($attribute): bool
{
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
return true;
}
protected function isValidAttributeAndValues(string $attribute, array $values): bool
{
if (!$this->isValidAttribute($attribute)) {
return false;
}
$attributeSchema = $this->schema[$attribute];
if (count($values) > $this->maxValuesCount) {
$this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute;
return false;
}
// Extract the type of desired attribute from collection $schema
$attributeType = $attributeSchema['type'];
foreach ($values as $value) {
$condition = match ($attributeType) {
Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING,
default => gettype($value) === $attributeType
};
if (!$condition) {
$this->message = 'Query type does not match expected: ' . $attributeType;
return false;
}
}
return true;
}
/**
* Is valid.
*
* Returns true if method is a filter method, attribute exists, and value matches attribute type
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
$attribute = $query->getAttribute();
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_NOTEQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSEREQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
$values = $query->getValues();
return $this->isValidAttributeAndValues($attribute, $values);
default:
return false;
}
}
public function getMethodType(): string
{
return self::METHOD_TYPE_FILTER;
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator\Range;
class Limit extends Base
{
protected int $maxLimit;
/**
* Query constructor
*
* @param int $maxLimit
*/
public function __construct(int $maxLimit = 100)
{
$this->maxLimit = $maxLimit;
}
protected function isValidLimit($limit): bool
{
$validator = new Range(0, $this->maxLimit);
if ($validator->isValid($limit)) {
return true;
}
$this->message = 'Invalid limit: ' . $validator->getDescription();
return false;
}
/**
* Is valid.
*
* Returns true if method is limit values are within range.
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method !== Query::TYPE_LIMIT) {
$this->message = 'Query method invalid: ' . $method;
return false;
}
$limit = $query->getValue();
return $this->isValidLimit($limit);
}
public function getMethodType(): string
{
return self::METHOD_TYPE_LIMIT;
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator\Range;
class Offset extends Base
{
protected int $maxOffset;
/**
* Query constructor
*
* @param int $maxOffset
*/
public function __construct(int $maxOffset = 5000)
{
$this->maxOffset = $maxOffset;
}
protected function isValidOffset($offset): bool
{
$validator = new Range(0, $this->maxOffset);
if ($validator->isValid($offset)) {
return true;
}
$this->message = 'Invalid offset: ' . $validator->getDescription();
return false;
}
/**
* Is valid.
*
* Returns true if method is offset and values are within range.
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
// Validate method
$method = $query->getMethod();
if ($method !== Query::TYPE_OFFSET) {
$this->message = 'Query method invalid: ' . $method;
return false;
}
$offset = $query->getValue();
return $this->isValidOffset($offset);
}
public function getMethodType(): string
{
return self::METHOD_TYPE_OFFSET;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Utopia\Database\Query;
use Utopia\Validator;
class Order extends Base
{
/**
* @var array
*/
protected $schema = [];
/**
* Query constructor
*
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
}
protected function isValidAttribute($attribute): bool
{
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
return true;
}
/**
* Is valid.
*
* Returns true if method is ORDER_ASC or ORDER_DESC and attributes are valid
*
* Otherwise, returns false
*
* @param Query $value
*
* @return bool
*/
public function isValid($query): bool
{
$method = $query->getMethod();
$attribute = $query->getAttribute();
if ($method === Query::TYPE_ORDERASC || $method === Query::TYPE_ORDERDESC) {
if ($attribute === '') {
return true;
}
return $this->isValidAttribute($attribute);
}
return false;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_ORDER;
}
}

View file

@ -75,6 +75,7 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
@ -103,6 +104,7 @@ class Response extends SwooleResponse
public const MODEL_USAGE_BUCKETS = 'usageBuckets';
public const MODEL_USAGE_STORAGE = 'usageStorage';
public const MODEL_USAGE_FUNCTIONS = 'usageFunctions';
public const MODEL_USAGE_FUNCTION = 'usageFunction';
public const MODEL_USAGE_PROJECT = 'usageProject';
// Database
@ -330,6 +332,7 @@ class Response extends SwooleResponse
->setModel(new UsageStorage())
->setModel(new UsageBuckets())
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageProject())
// Verification
// Recovery

View file

@ -0,0 +1,97 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageFunction extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('executionsTotal', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of function executions.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('executionsFailure', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution failures.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('executionsSuccess', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution successes.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('executionsTime', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution duration.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsTotal', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of function builds.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsFailure', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build failures.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsSuccess', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build successes.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsTime', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build duration.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'UsageFunction';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_USAGE_FUNCTION;
}
}

View file

@ -16,27 +16,62 @@ class UsageFunctions extends Model
'default' => '',
'example' => '30d',
])
->addRule('functionsExecutions', [
->addRule('executionsTotal', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'description' => 'Aggregated stats for number of function executions.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('functionsFailures', [
->addRule('executionsFailure', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution failures.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('functionsCompute', [
->addRule('executionsSuccess', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution successes.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('executionsTime', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution duration.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsTotal', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of function builds.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsFailure', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build failures.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsSuccess', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build successes.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('buildsTime', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function build duration.',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
;
}

View file

@ -30,7 +30,7 @@ class UsageProject extends Model
'example' => new \stdClass(),
'array' => true
])
->addRule('functions', [
->addRule('executions', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],

View file

@ -16,16 +16,9 @@ class UsageStorage extends Model
'default' => '',
'example' => '30d',
])
->addRule('filesStorage', [
->addRule('storage', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size by files (in bytes).',
'default' => [],
'example' => new \stdClass(),
'array' => true
])
->addRule('tagsStorage', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size by tags (in bytes).',
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
'default' => [],
'example' => new \stdClass(),
'array' => true

View file

@ -0,0 +1,570 @@
<?php
namespace Tests\E2E\General;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use CURLFile;
use Tests\E2E\Services\Functions\FunctionsBase;
class UsageTest extends Scope
{
use ProjectCustom;
use SideServer;
use FunctionsBase;
protected string $projectId;
protected function setUp(): void
{
parent::setUp();
}
public function testUsersStats(): array
{
$project = $this->getProject(true);
$projectId = $project['$id'];
$headers['x-appwrite-project'] = $project['$id'];
$headers['x-appwrite-key'] = $project['apiKey'];
$headers['content-type'] = 'application/json';
$usersCount = 0;
$requestsCount = 0;
for ($i = 0; $i < 10; $i++) {
$email = uniqid() . 'user@usage.test';
$password = 'password';
$name = uniqid() . 'User';
$res = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => 'unique()',
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals($email, $res['body']['email']);
$this->assertNotEmpty($res['body']['$id']);
$usersCount++;
$requestsCount++;
if ($i < 5) {
$userId = $res['body']['$id'];
$res = $this->client->call(Client::METHOD_GET, '/users/' . $userId, $headers);
$this->assertEquals($userId, $res['body']['$id']);
$res = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, $headers);
$this->assertEmpty($res['body']);
$requestsCount += 2;
$usersCount--;
}
}
sleep(35);
// console request
$cheaders = [
'origin' => 'http://localhost',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
];
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $cheaders);
$res = $res['body'];
$this->assertEquals(8, count($res));
$this->assertEquals(30, count($res['requests']));
$this->assertEquals(30, count($res['users']));
$this->assertEquals($usersCount, $res['users'][array_key_last($res['users'])]['value']);
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/users/usage?range=30d', array_merge($cheaders, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals(10, $res['usersCreate'][array_key_last($res['usersCreate'])]['value']);
$this->assertEquals(5, $res['usersRead'][array_key_last($res['usersRead'])]['value']);
$this->assertEquals(5, $res['usersDelete'][array_key_last($res['usersDelete'])]['value']);
return ['projectId' => $projectId, 'headers' => $headers, 'requestsCount' => $requestsCount];
}
/** @depends testUsersStats */
public function testStorageStats(array $data): array
{
$projectId = $data['projectId'];
$headers = $data['headers'];
$bucketId = '';
$bucketsCount = 0;
$requestsCount = $data['requestsCount'];
$storageTotal = 0;
$bucketsCreate = 0;
$bucketsDelete = 0;
$bucketsRead = 0;
$filesCount = 0;
$filesRead = 0;
$filesCreate = 0;
$filesDelete = 0;
for ($i = 0; $i < 10; $i++) {
$name = uniqid() . ' bucket';
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [
'bucketId' => 'unique()',
'name' => $name,
'permission' => 'bucket'
]);
$this->assertEquals($name, $res['body']['name']);
$this->assertNotEmpty($res['body']['$id']);
$bucketId = $res['body']['$id'];
$bucketsCreate++;
$bucketsCount++;
$requestsCount++;
if ($i < 5) {
$res = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, $headers);
$this->assertEquals($bucketId, $res['body']['$id']);
$bucketsRead++;
$res = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, $headers);
$this->assertEmpty($res['body']);
$bucketsDelete++;
$requestsCount += 2;
$bucketsCount--;
}
}
// upload some files
$files = [
[
'path' => realpath(__DIR__ . '/../../resources/logo.png'),
'name' => 'logo.png',
],
[
'path' => realpath(__DIR__ . '/../../resources/file.png'),
'name' => 'file.png',
],
[
'path' => realpath(__DIR__ . '/../../resources/disk-a/kitten-3.gif'),
'name' => 'kitten-3.gif',
],
[
'path' => realpath(__DIR__ . '/../../resources/disk-a/kitten-1.jpg'),
'name' => 'kitten-1.jpg',
],
];
for ($i = 0; $i < 10; $i++) {
$file = $files[$i % count($files)];
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge($headers, ['content-type' => 'multipart/form-data']), [
'fileId' => 'unique()',
'file' => new CURLFile($file['path'], '', $file['name']),
]);
$this->assertNotEmpty($res['body']['$id']);
$fileSize = $res['body']['sizeOriginal'];
$storageTotal += $fileSize;
$filesCount++;
$filesCreate++;
$requestsCount++;
$fileId = $res['body']['$id'];
if ($i < 5) {
$res = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, $headers);
$this->assertEquals($fileId, $res['body']['$id']);
$filesRead++;
$res = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, $headers);
$this->assertEmpty($res['body']);
$filesDelete++;
$requestsCount += 2;
$filesCount--;
$storageTotal -= $fileSize;
}
}
sleep(35);
// console request
$headers = [
'origin' => 'http://localhost',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
];
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $headers);
$res = $res['body'];
$this->assertEquals(8, count($res));
$this->assertEquals(30, count($res['requests']));
$this->assertEquals(30, count($res['storage']));
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
$this->assertEquals($storageTotal, $res['storage'][array_key_last($res['storage'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/storage/usage?range=30d', array_merge($headers, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals($storageTotal, $res['storage'][array_key_last($res['storage'])]['value']);
$this->assertEquals($bucketsCount, $res['bucketsCount'][array_key_last($res['bucketsCount'])]['value']);
$this->assertEquals($bucketsRead, $res['bucketsRead'][array_key_last($res['bucketsRead'])]['value']);
$this->assertEquals($bucketsCreate, $res['bucketsCreate'][array_key_last($res['bucketsCreate'])]['value']);
$this->assertEquals($bucketsDelete, $res['bucketsDelete'][array_key_last($res['bucketsDelete'])]['value']);
$this->assertEquals($filesCount, $res['filesCount'][array_key_last($res['filesCount'])]['value']);
$this->assertEquals($filesRead, $res['filesRead'][array_key_last($res['filesRead'])]['value']);
$this->assertEquals($filesCreate, $res['filesCreate'][array_key_last($res['filesCreate'])]['value']);
$this->assertEquals($filesDelete, $res['filesDelete'][array_key_last($res['filesDelete'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/storage/' . $bucketId . '/usage?range=30d', array_merge($headers, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals($storageTotal, $res['filesStorage'][array_key_last($res['filesStorage'])]['value']);
$this->assertEquals($filesCount, $res['filesCount'][array_key_last($res['filesCount'])]['value']);
$this->assertEquals($filesRead, $res['filesRead'][array_key_last($res['filesRead'])]['value']);
$this->assertEquals($filesCreate, $res['filesCreate'][array_key_last($res['filesCreate'])]['value']);
$this->assertEquals($filesDelete, $res['filesDelete'][array_key_last($res['filesDelete'])]['value']);
$data['requestsCount'] = $requestsCount;
return $data;
}
/** @depends testStorageStats */
public function testDatabaseStats(array $data): array
{
$headers = $data['headers'];
$projectId = $data['projectId'];
$databaseId = '';
$collectionId = '';
$requestsCount = $data['requestsCount'];
$databasesCount = 0;
$databasesCreate = 0;
$databasesRead = 0;
$databasesDelete = 0;
$collectionsCount = 0;
$collectionsCreate = 0;
$collectionsRead = 0;
$collectionsUpdate = 0;
$collectionsDelete = 0;
$documentsCount = 0;
$documentsCreate = 0;
$documentsRead = 0;
$documentsDelete = 0;
for ($i = 0; $i < 10; $i++) {
$name = uniqid() . ' database';
$res = $this->client->call(Client::METHOD_POST, '/databases', $headers, [
'databaseId' => 'unique()',
'name' => $name,
]);
$this->assertEquals($name, $res['body']['name']);
$this->assertNotEmpty($res['body']['$id']);
$databaseId = $res['body']['$id'];
$requestsCount++;
$databasesCount++;
$databasesCreate++;
if ($i < 5) {
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, $headers);
$this->assertEquals($databaseId, $res['body']['$id']);
$databasesRead++;
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $headers);
$this->assertEmpty($res['body']);
$databasesDelete++;
$databasesCount--;
$requestsCount += 2;
}
}
for ($i = 0; $i < 10; $i++) {
$name = uniqid() . ' collection';
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $headers, [
'collectionId' => 'unique()',
'name' => $name,
'permission' => 'collection',
'read' => ['role:all'],
'write' => ['role:all']
]);
$this->assertEquals($name, $res['body']['name']);
$this->assertNotEmpty($res['body']['$id']);
$collectionId = $res['body']['$id'];
$requestsCount++;
$collectionsCount++;
$collectionsCreate++;
if ($i < 5) {
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, $headers);
$this->assertEquals($collectionId, $res['body']['$id']);
$collectionsRead++;
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, $headers);
$this->assertEmpty($res['body']);
$collectionsDelete++;
$collectionsCount--;
$requestsCount += 2;
}
}
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes' . '/string', $headers, [
'key' => 'name',
'size' => 255,
'required' => true,
]);
$this->assertEquals('name', $res['body']['key']);
$collectionsUpdate++;
$requestsCount++;
sleep(10);
for ($i = 0; $i < 10; $i++) {
$name = uniqid() . ' collection';
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [
'documentId' => 'unique()',
'data' => ['name' => $name]
]);
$this->assertEquals($name, $res['body']['name']);
$this->assertNotEmpty($res['body']['$id']);
$documentId = $res['body']['$id'];
$requestsCount++;
$documentsCount++;
$documentsCreate++;
if ($i < 5) {
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers);
$this->assertEquals($documentId, $res['body']['$id']);
$documentsRead++;
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers);
$this->assertEmpty($res['body']);
$documentsDelete++;
$documentsCount--;
$requestsCount += 2;
}
}
sleep(35);
// check datbase stats
$headers = [
'origin' => 'http://localhost',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
];
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $headers);
$res = $res['body'];
$this->assertEquals(8, count($res));
$this->assertEquals(30, count($res['requests']));
$this->assertEquals(30, count($res['storage']));
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
$this->assertEquals($collectionsCount, $res['collections'][array_key_last($res['collections'])]['value']);
$this->assertEquals($documentsCount, $res['documents'][array_key_last($res['documents'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/databases/usage?range=30d', array_merge($headers, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals($databasesCount, $res['databasesCount'][array_key_last($res['databasesCount'])]['value']);
$this->assertEquals($collectionsCount, $res['collectionsCount'][array_key_last($res['collectionsCount'])]['value']);
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
$this->assertEquals($databasesCreate, $res['databasesCreate'][array_key_last($res['databasesCreate'])]['value']);
$this->assertEquals($databasesRead, $res['databasesRead'][array_key_last($res['databasesRead'])]['value']);
$this->assertEquals($databasesDelete, $res['databasesDelete'][array_key_last($res['databasesDelete'])]['value']);
$this->assertEquals($collectionsCreate, $res['collectionsCreate'][array_key_last($res['collectionsCreate'])]['value']);
$this->assertEquals($collectionsRead, $res['collectionsRead'][array_key_last($res['collectionsRead'])]['value']);
$this->assertEquals($collectionsUpdate, $res['collectionsUpdate'][array_key_last($res['collectionsUpdate'])]['value']);
$this->assertEquals($collectionsDelete, $res['collectionsDelete'][array_key_last($res['collectionsDelete'])]['value']);
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/usage?range=30d', array_merge($headers, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals($collectionsCount, $res['collectionsCount'][array_key_last($res['collectionsCount'])]['value']);
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
$this->assertEquals($collectionsCreate, $res['collectionsCreate'][array_key_last($res['collectionsCreate'])]['value']);
$this->assertEquals($collectionsRead, $res['collectionsRead'][array_key_last($res['collectionsRead'])]['value']);
$this->assertEquals($collectionsUpdate, $res['collectionsUpdate'][array_key_last($res['collectionsUpdate'])]['value']);
$this->assertEquals($collectionsDelete, $res['collectionsDelete'][array_key_last($res['collectionsDelete'])]['value']);
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/usage?range=30d', array_merge($headers, [
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin'
]));
$res = $res['body'];
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
$data['requestsCount'] = $requestsCount;
return $data;
}
/** @depends testDatabaseStats */
public function testFunctionsStats(array $data): void
{
$headers = $data['headers'];
$functionId = '';
$executionTime = 0;
$executions = 0;
$failures = 0;
$response1 = $this->client->call(Client::METHOD_POST, '/functions', $headers, [
'functionId' => 'unique()',
'name' => 'Test',
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
'funcKey3' => 'funcValue3',
],
'events' => [
'users.*.create',
'users.*.delete',
],
'schedule' => '0 0 1 1 *',
'timeout' => 10,
]);
$functionId = $response1['body']['$id'] ?? '';
$this->assertEquals(201, $response1['headers']['status-code']);
$this->assertNotEmpty($response1['body']['$id']);
$code = realpath(__DIR__ . '/../../resources/functions') . "/php/code.tar.gz";
$this->packageCode('php');
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge($headers, ['content-type' => 'multipart/form-data',]), [
'entrypoint' => 'index.php',
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
'activate' => true
]);
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertIsInt($deployment['body']['$createdAt']);
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
// Wait for deployment to build.
sleep(30);
$response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, $headers, []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertIsInt($response['body']['$createdAt']);
$this->assertIsInt($response['body']['$updatedAt']);
$this->assertEquals($deploymentId, $response['body']['deployment']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', $headers, [
'async' => false,
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertEquals($functionId, $execution['body']['functionId']);
$executionTime += (int) ($execution['body']['time'] * 1000);
if ($execution['body']['status'] == 'failed') {
$failures++;
} elseif ($execution['body']['status'] == 'completed') {
$executions++;
}
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', $headers, [
'async' => false,
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertEquals($functionId, $execution['body']['functionId']);
if ($execution['body']['status'] == 'failed') {
$failures++;
} elseif ($execution['body']['status'] == 'completed') {
$executions++;
}
$executionTime += (int) ($execution['body']['time'] * 1000);
sleep(25);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', $headers, [
'range' => '30d'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(9, count($response['body']));
$this->assertEquals('30d', $response['body']['range']);
$this->assertIsArray($response['body']['executionsTotal']);
$this->assertIsArray($response['body']['executionsFailure']);
$this->assertIsArray($response['body']['executionsSuccess']);
$this->assertIsArray($response['body']['executionsTime']);
$this->assertIsArray($response['body']['buildsTotal']);
$this->assertIsArray($response['body']['buildsFailure']);
$this->assertIsArray($response['body']['buildsSuccess']);
$this->assertIsArray($response['body']['buildsTime']);
$response = $response['body'];
$this->assertEquals($executions, $response['executionsTotal'][array_key_last($response['executionsTotal'])]['value']);
$this->assertEquals($executionTime, $response['executionsTime'][array_key_last($response['executionsTime'])]['value']);
$this->assertEquals($failures, $response['executionsFailure'][array_key_last($response['executionsFailure'])]['value']);
$response = $this->client->call(Client::METHOD_GET, '/functions/usage', $headers, [
'range' => '30d'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(9, count($response['body']));
$this->assertEquals($response['body']['range'], '30d');
$this->assertIsArray($response['body']['executionsTotal']);
$this->assertIsArray($response['body']['executionsFailure']);
$this->assertIsArray($response['body']['executionsSuccess']);
$this->assertIsArray($response['body']['executionsTime']);
$this->assertIsArray($response['body']['buildsTotal']);
$this->assertIsArray($response['body']['buildsFailure']);
$this->assertIsArray($response['body']['buildsSuccess']);
$this->assertIsArray($response['body']['buildsTime']);
$response = $response['body'];
$this->assertEquals($executions, $response['executionsTotal'][array_key_last($response['executionsTotal'])]['value']);
$this->assertEquals($executionTime, $response['executionsTime'][array_key_last($response['executionsTime'])]['value']);
$this->assertGreaterThan(0, $response['buildsTime'][array_key_last($response['buildsTime'])]['value']);
$this->assertEquals($failures, $response['executionsFailure'][array_key_last($response['executionsFailure'])]['value']);
}
protected function tearDown(): void
{
$this->usersCount = 0;
$this->requestsCount = 0;
$projectId = '';
$headers = [];
}
}

View file

@ -13,11 +13,12 @@ trait ProjectCustom
protected static $project = [];
/**
* @param bool $fresh
* @return array
*/
public function getProject(): array
public function getProject(bool $fresh = false): array
{
if (!empty(self::$project)) {
if (!empty(self::$project) && !$fresh) {
return self::$project;
}
@ -116,13 +117,17 @@ trait ProjectCustom
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertNotEmpty($webhook['body']);
self::$project = [
$project = [
'$id' => $project['body']['$id'],
'name' => $project['body']['name'],
'apiKey' => $key['body']['secret'],
'webhookId' => $webhook['body']['$id'],
'signatureKey' => $webhook['body']['signatureKey'],
];
if ($fresh) {
return $project;
}
self::$project = $project;
return self::$project;
}

View file

@ -390,7 +390,7 @@ trait AccountBase
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'limit' => 1
'queries' => [ 'limit(1)' ],
]);
$this->assertEquals($responseLimit['headers']['status-code'], 200);
@ -407,7 +407,7 @@ trait AccountBase
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'offset' => 1
'queries' => [ 'offset(1)' ],
]);
$this->assertEquals($responseOffset['headers']['status-code'], 200);
@ -424,8 +424,7 @@ trait AccountBase
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'limit' => 1,
'offset' => 1
'queries' => [ 'limit(1)', 'offset(1)' ],
]);
$this->assertEquals($responseLimitOffset['headers']['status-code'], 200);

View file

@ -614,7 +614,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'search' => $newName
'search' => $newName,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -628,7 +628,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'search' => $id
'search' => $id,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -654,7 +654,8 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'search' => '"' . $email . '"'
'search' => '"' . $email . '"',
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -668,7 +669,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'search' => $id
'search' => $id,
]);
$this->assertEquals($response['headers']['status-code'], 200);

View file

@ -988,8 +988,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'queries' => [ 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1009,8 +1008,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['DESC'],
'queries' => [ 'orderDesc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1125,7 +1123,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['documents'][0]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['documents'][0]['$id'] . '")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1137,7 +1135,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['documents'][2]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['documents'][2]['$id'] . '")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1150,8 +1148,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'queries' => [ 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $base['headers']['status-code']);
@ -1164,9 +1161,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'cursor' => $base['body']['documents'][1]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['documents'][1]['$id'] . '")', 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1180,8 +1175,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['DESC'],
'queries' => [ 'orderDesc("releaseYear")' ],
]);
$this->assertEquals(200, $base['headers']['status-code']);
@ -1194,9 +1188,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['DESC'],
'cursor' => $base['body']['documents'][1]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['documents'][1]['$id'] . '")', 'orderDesc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1210,7 +1202,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown'
'queries' => [ 'cursorAfter("unknown")' ],
]);
$this->assertEquals(400, $documents['headers']['status-code']);
@ -1242,8 +1234,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['documents'][2]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['documents'][2]['$id'] . '")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1255,8 +1246,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['documents'][0]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['documents'][0]['$id'] . '")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1269,8 +1259,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'queries' => [ 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $base['headers']['status-code']);
@ -1283,10 +1272,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'cursor' => $base['body']['documents'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['documents'][1]['$id'] . '")', 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1300,8 +1286,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['DESC'],
'queries' => [ 'orderDesc("releaseYear")' ],
]);
$this->assertEquals(200, $base['headers']['status-code']);
@ -1314,10 +1299,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['DESC'],
'cursor' => $base['body']['documents'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['documents'][1]['$id'] . '")', 'orderDesc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1337,9 +1319,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1,
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'queries' => [ 'limit(1)', 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
@ -1350,10 +1330,7 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 2,
'offset' => 1,
'orderAttributes' => ['releaseYear'],
'orderTypes' => ['ASC'],
'queries' => [ 'limit(2)', 'offset(1)', 'orderAsc("releaseYear")' ],
]);
$this->assertEquals(200, $documents['headers']['status-code']);

View file

@ -50,15 +50,61 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals($test1['body']['$id'], $databases['body']['databases'][0]['$id']);
$this->assertEquals($test2['body']['$id'], $databases['body']['databases'][1]['$id']);
/**
* Test for Order
*/
$base = array_reverse($databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderType' => 'DESC'
'queries' => [ 'limit(1)' ],
]);
$this->assertEquals(200, $databases['headers']['status-code']);
$this->assertCount(1, $databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ],
]);
$this->assertEquals(200, $databases['headers']['status-code']);
$this->assertCount(1, $databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("name", ["Test 1", "Test 2"])' ],
]);
$this->assertEquals(200, $databases['headers']['status-code']);
$this->assertCount(2, $databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("name", "Test 2")' ],
]);
$this->assertEquals(200, $databases['headers']['status-code']);
$this->assertCount(1, $databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("$id", "first")' ],
]);
$this->assertEquals(200, $databases['headers']['status-code']);
$this->assertCount(1, $databases['body']['databases']);
/**
* Test for Order
*/
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'orderDesc("$id")' ],
]);
$this->assertEquals(2, $databases['body']['total']);
@ -77,7 +123,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['databases'][0]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['databases'][0]['$id'] . '")' ],
]);
$this->assertCount(1, $databases['body']['databases']);
@ -87,7 +133,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['databases'][1]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['databases'][1]['$id'] . '")' ],
]);
$this->assertCount(0, $databases['body']['databases']);
@ -105,8 +151,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['databases'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['databases'][1]['$id'] . '")' ],
]);
$this->assertCount(1, $databases['body']['databases']);
@ -116,8 +161,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['databases'][0]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['databases'][0]['$id'] . '")' ],
]);
$this->assertCount(0, $databases['body']['databases']);
@ -163,7 +207,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown',
'queries' => [ 'cursorAfter("unknown")' ],
]);
$this->assertEquals(400, $response['headers']['status-code']);
@ -287,15 +331,56 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals($test1['body']['$id'], $collections['body']['collections'][0]['$id']);
$this->assertEquals($test2['body']['$id'], $collections['body']['collections'][1]['$id']);
/**
* Test for Order
*/
$base = array_reverse($collections['body']['collections']);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'orderType' => 'DESC'
'queries' => [ 'limit(1)' ]
]);
$this->assertEquals(200, $collections['headers']['status-code']);
$this->assertCount(1, $collections['body']['collections']);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals(200, $collections['headers']['status-code']);
$this->assertCount(1, $collections['body']['collections']);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("enabled", true)' ]
]);
$this->assertEquals(200, $collections['headers']['status-code']);
$this->assertCount(2, $collections['body']['collections']);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("enabled", false)' ]
]);
$this->assertEquals(200, $collections['headers']['status-code']);
$this->assertCount(0, $collections['body']['collections']);
/**
* Test for Order
*/
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'orderDesc("$id")' ],
]);
$this->assertEquals(2, $collections['body']['total']);
@ -314,7 +399,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['collections'][0]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['collections'][0]['$id'] . '")' ],
]);
$this->assertCount(1, $collections['body']['collections']);
@ -324,7 +409,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['collections'][1]['$id']
'queries' => [ 'cursorAfter("' . $base['body']['collections'][1]['$id'] . '")' ],
]);
$this->assertCount(0, $collections['body']['collections']);
@ -342,8 +427,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['collections'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['collections'][1]['$id'] . '")' ],
]);
$this->assertCount(1, $collections['body']['collections']);
@ -353,8 +437,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $base['body']['collections'][0]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['collections'][0]['$id'] . '")' ],
]);
$this->assertCount(0, $collections['body']['collections']);
@ -400,7 +483,7 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown',
'queries' => [ 'cursorAfter("unknown")' ],
]);
$this->assertEquals(400, $response['headers']['status-code']);

View file

@ -342,7 +342,51 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'cursor' => $base['body']['executions'][0]['$id']
'queries' => [ 'limit(1)' ]
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'queries' => [ 'equal("status", ["completed"])' ]
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(2, $executions['body']['executions']);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'queries' => [ 'equal("status", ["failed"])' ]
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(0, $executions['body']['executions']);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'queries' => [ 'cursorAfter("' . $base['body']['executions'][0]['$id'] . '")' ],
]);
$this->assertCount(1, $executions['body']['executions']);
@ -353,8 +397,7 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'cursor' => $base['body']['executions'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $base['body']['executions'][1]['$id'] . '")' ],
]);
// Cleanup : Delete function

View file

@ -115,6 +115,46 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals($response['body']['functions'][0]['name'], 'Test');
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'limit(0)' ]
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(0, $response['body']['functions']);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(0, $response['body']['functions']);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("status", "disabled")' ]
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(1, $response['body']['functions']);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("status", "enabled")' ]
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(0, $response['body']['functions']);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -201,7 +241,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $functions['body']['functions'][0]['$id']
'queries' => [ 'cursorAfter("' . $functions['body']['functions'][0]['$id'] . '")' ],
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -212,8 +252,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $functions['body']['functions'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'cursorBefore("' . $functions['body']['functions'][1]['$id'] . '")' ],
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -227,7 +266,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown',
'queries' => [ 'cursorAfter("unknown")' ],
]);
$this->assertEquals($response['headers']['status-code'], 400);
@ -445,6 +484,46 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(2, $function['body']['deployments']);
$this->assertEquals($function['body']['deployments'][0]['$id'], $data['deploymentId']);
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'limit(1)' ]
]);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertCount(1, $function['body']['deployments']);
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertCount(1, $function['body']['deployments']);
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("entrypoint", "index.php")' ]
]);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertCount(2, $function['body']['deployments']);
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("entrypoint", "index.js")' ]
]);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertCount(0, $function['body']['deployments']);
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -581,6 +660,36 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $function['body']['executions']);
$this->assertEquals($function['body']['executions'][0]['$id'], $data['executionId']);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'limit(0)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(0, $response['body']['executions']);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(0, $response['body']['executions']);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("trigger", "http")' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['executions']);
/**
* Test search queries
*/

View file

@ -276,7 +276,7 @@ class ProjectsConsoleClientTest extends Scope
$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']['executions']);
$this->assertIsArray($response['body']['documents']);
$this->assertIsArray($response['body']['collections']);
$this->assertIsArray($response['body']['users']);

View file

@ -259,6 +259,42 @@ trait StorageBase
$this->assertGreaterThan(0, $files['body']['total']);
$this->assertGreaterThan(0, count($files['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'limit(0)' ]
]);
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertEquals(0, count($files['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertEquals(0, count($files['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("mimeType", "image/png")' ]
]);
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertEquals(1, count($files['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("mimeType", "image/jpeg")' ]
]);
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertEquals(0, count($files['body']['files']));
/**
* Test for FAILURE unknown Bucket
*/

View file

@ -39,9 +39,9 @@ class StorageConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 13);
$this->assertEquals(12, count($response['body']));
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['filesStorage']);
$this->assertIsArray($response['body']['storage']);
$this->assertIsArray($response['body']['filesCount']);
}

View file

@ -98,7 +98,47 @@ class StorageCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $response['body']['buckets'][0]['$id'],
'queries' => [ 'limit(1)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['buckets']);
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['buckets']);
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("$id", "bucket1")' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['buckets']);
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("fileSecurity", true)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['buckets']);
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'cursorAfter("' . $response['body']['buckets'][0]['$id'] . '")' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);

View file

@ -128,25 +128,41 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 2,
'queries' => [ 'limit(2)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, $response['body']['total']);
$this->assertIsInt($response['body']['total']);
$this->assertCount(2, $response['body']['teams']);
$this->assertEquals(2, count($response['body']['teams']));
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1,
'queries' => [ 'offset(1)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, $response['body']['total']);
$this->assertIsInt($response['body']['total']);
$this->assertGreaterThan(2, $response['body']['teams']);
$this->assertGreaterThan(1, count($response['body']['teams']));
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'greaterThanEqual("total", 0)' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(2, count($response['body']['teams']));
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("name", ["Arsenal", "Newcastle"])' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, count($response['body']['teams']));
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
@ -191,7 +207,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 2,
'queries' => [ 'limit(2)' ],
]);
$this->assertEquals(200, $teams['headers']['status-code']);
@ -203,8 +219,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1,
'cursor' => $teams['body']['teams'][0]['$id']
'queries' => [ 'limit(1)', 'cursorAfter("' . $teams['body']['teams'][0]['$id'] . '")' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
@ -217,9 +232,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1,
'cursor' => $teams['body']['teams'][1]['$id'],
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => [ 'limit(1)', 'cursorBefore("' . $teams['body']['teams'][1]['$id'] . '")' ],
]);
$this->assertEquals(200, $response['headers']['status-code']);
@ -235,7 +248,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown'
'queries' => [ 'cursorAfter("unknown")' ],
]);
$this->assertEquals(400, $response['headers']['status-code']);

View file

@ -34,6 +34,46 @@ trait TeamsBaseClient
$membershipId = $response['body']['memberships'][0]['$id'];
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'limit(0)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(0, $response['body']['memberships']);
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(0, $response['body']['memberships']);
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("confirm", true)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['memberships']);
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [ 'equal("confirm", false)' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(0, $response['body']['memberships']);
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -209,7 +249,7 @@ trait TeamsBaseClient
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $memberships['body']['memberships'][0]['$id']
'queries' => [ 'cursorAfter("' . $memberships['body']['memberships'][0]['$id'] . '")' ]
]);
$this->assertEquals(200, $response['headers']['status-code']);

View file

@ -3,7 +3,6 @@
namespace Tests\E2E\Services\Users;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\ID;
trait UsersBase
@ -379,11 +378,145 @@ trait UsersBase
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][1]['$id'], 'user1');
$user1 = $response['body']['users'][1];
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => $response['body']['users'][0]['$id']
'queries' => ['equal("name", "' . $user1['name'] . '")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['name'], $user1['name']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("email", "' . $user1['email'] . '")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['email'], $user1['email']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("status", true)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][0]['status'], $user1['status']);
$this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']);
$this->assertEquals($response['body']['users'][1]['status'], $user1['status']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("status", false)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertEmpty($response['body']['users']);
$this->assertCount(0, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("passwordUpdate", "' . $user1['passwordUpdate'] . '")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['passwordUpdate'], $user1['passwordUpdate']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("registration", "' . $user1['registration'] . '")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['registration'], $user1['registration']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("emailVerification", false)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][0]['status'], $user1['status']);
$this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']);
$this->assertEquals($response['body']['users'][1]['status'], $user1['status']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("emailVerification", true)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertEmpty($response['body']['users']);
$this->assertCount(0, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("phoneVerification", false)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertIsArray($response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("phoneVerification", true)']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertEmpty($response['body']['users']);
$this->assertCount(0, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAfter("' . $data['userId'] . '")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -396,8 +529,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'user1',
'cursorDirection' => Database::CURSOR_BEFORE
'queries' => ['cursorBefore("user1")']
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -413,7 +545,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Ronaldo'
'search' => "Ronaldo",
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
@ -425,7 +557,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'cristiano.ronaldo@manchester-united.co.uk'
'search' => "cristiano.ronaldo@manchester-united.co.uk",
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
@ -437,7 +569,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'cristiano.ronaldo'
'search' => "cristiano.ronaldo",
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
@ -449,7 +581,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'manchester'
'search' => "manchester",
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
@ -461,7 +593,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'united.co.uk'
'search' => "united.co.uk",
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -475,7 +607,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'man'
'search' => "man",
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -489,7 +621,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['userId']
'search' => $data['userId'],
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -505,7 +637,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'cursor' => 'unknown'
'queries' => ['cursorAfter("unknown")']
]);
$this->assertEquals(400, $response['headers']['status-code']);
@ -596,7 +728,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $newName
'search' => $newName,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -609,7 +741,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $id
'search' => $id,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -663,7 +795,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $newEmail
'search' => $newEmail,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -676,7 +808,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $id
'search' => $id,
]);
$this->assertEquals($response['headers']['status-code'], 200);
@ -852,7 +984,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1
'queries' => [ 'limit(1)' ]
]);
$this->assertEquals($logs['headers']['status-code'], 200);
@ -864,7 +996,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1
'queries' => [ 'offset(1)' ]
]);
$this->assertEquals($logs['headers']['status-code'], 200);
@ -875,14 +1007,79 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1,
'limit' => 1
'queries' => [ 'limit(1)', 'offset(1)' ]
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(101)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(5001)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
/**

View file

@ -2,7 +2,7 @@
namespace Tests\Unit\Stats;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use PHPUnit\Framework\TestCase;
use Utopia\App;

View file

@ -0,0 +1,121 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator;
use Appwrite\Utopia\Database\Validator\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Database;
use Utopia\Database\Document;
class IndexedQueriesTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testEmptyQueries(): void
{
$validator = new IndexedQueries();
$this->assertEquals(true, $validator->isValid([]));
}
public function testInvalidQuery(): void
{
$validator = new IndexedQueries();
$this->assertEquals(false, $validator->isValid(["this.is.invalid"]));
}
public function testInvalidMethod(): void
{
$validator = new IndexedQueries();
$this->assertEquals(false, $validator->isValid(['equal("attr", "value")']));
$validator = new IndexedQueries([], [], new Limit());
$this->assertEquals(false, $validator->isValid(['equal("attr", "value")']));
}
public function testInvalidValue(): void
{
$validator = new IndexedQueries([], [], new Limit());
$this->assertEquals(false, $validator->isValid(['limit(-1)']));
}
public function testValid(): void
{
$attributes = [
new Document([
'key' => 'name',
'type' => Database::VAR_STRING,
'array' => false,
]),
];
$indexes = [
new Document([
'status' => 'available',
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
]),
new Document([
'status' => 'available',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
]),
];
$validator = new IndexedQueries(
$attributes,
$indexes,
new Cursor(),
new Filter($attributes),
new Limit(),
new Offset(),
new Order($attributes),
);
$this->assertEquals(true, $validator->isValid(['cursorAfter("asdf")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("name", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['limit(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['offset(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['orderAsc("name")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['search("name", "value")']), $validator->getDescription());
}
public function testMissingIndex(): void
{
$attributes = [
new Document([
'key' => 'name',
'type' => Database::VAR_STRING,
'array' => false,
]),
];
$indexes = [
new Document([
'status' => 'available',
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
]),
];
$validator = new IndexedQueries(
$attributes,
$indexes,
new Cursor(),
new Filter($attributes),
new Limit(),
new Offset(),
new Order($attributes),
);
$this->assertEquals(false, $validator->isValid(['equal("dne", "value")']), $validator->getDescription());
$this->assertEquals(false, $validator->isValid(['orderAsc("dne")']), $validator->getDescription());
$this->assertEquals(false, $validator->isValid(['search("name", "value")']), $validator->getDescription());
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Base;
use PHPUnit\Framework\TestCase;
class CollectionTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testEmptyQueries(): void
{
$validator = new Base('users', []);
$this->assertEquals($validator->isValid([]), true);
}
public function testValid(): void
{
$validator = new Base('users', ['name', 'search']);
$this->assertEquals(true, $validator->isValid(['cursorAfter("asdf")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("name", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['limit(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['offset(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['orderAsc("name")']), $validator->getDescription());
}
public function testMissingIndex(): void
{
$validator = new Base('users', ['name']);
$this->assertEquals(false, $validator->isValid(['equal("dne", "value")']), $validator->getDescription());
$this->assertEquals(false, $validator->isValid(['orderAsc("dne")']), $validator->getDescription());
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use PHPUnit\Framework\TestCase;
class UsersTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testIsValid(): void
{
$validator = new Users();
/**
* Test for Success
*/
$this->assertEquals(true, $validator->isValid([]), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("name", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("email", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("phone", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['greaterThan("passwordUpdate", "2020-10-15 06:38")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['greaterThan("registration", "2020-10-15 06:38")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("emailVerification", true)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("phoneVerification", true)']), $validator->getDescription());
/**
* Test for Failure
*/
$this->assertEquals(false, $validator->isValid(['equal("password", "value")']), $validator->getDescription());
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Database;
use Utopia\Database\Document;
class QueriesTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testEmptyQueries(): void
{
$validator = new Queries();
$this->assertEquals(true, $validator->isValid([]));
}
public function testInvalidQuery(): void
{
$validator = new Queries();
$this->assertEquals(false, $validator->isValid(["this.is.invalid"]));
}
public function testInvalidMethod(): void
{
$validator = new Queries();
$this->assertEquals(false, $validator->isValid(['equal("attr", "value")']));
$validator = new Queries(new Limit());
$this->assertEquals(false, $validator->isValid(['equal("attr", "value")']));
}
public function testInvalidValue(): void
{
$validator = new Queries(new Limit());
$this->assertEquals(false, $validator->isValid(['limit(-1)']));
}
public function testValid(): void
{
$attributes = [
new Document([
'key' => 'name',
'type' => Database::VAR_STRING,
'array' => false,
])
];
$validator = new Queries(
new Cursor(),
new Filter($attributes),
new Limit(),
new Offset(),
new Order($attributes),
);
$this->assertEquals(true, $validator->isValid(['cursorAfter("asdf")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['equal("name", "value")']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['limit(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['offset(10)']), $validator->getDescription());
$this->assertEquals(true, $validator->isValid(['orderAsc("name")']), $validator->getDescription());
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class CursorTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Cursor();
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(new Query(Query::TYPE_CURSORAFTER, values: ['asdf'])), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(new Query(Query::TYPE_CURSORBEFORE, values: ['asdf'])), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5001)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('attr', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderAsc('attr')), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('attr')), false, $this->validator->getDescription());
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class FilterTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Filter(
attributes: [
new Document([
'key' => 'attr',
'type' => Database::VAR_STRING,
'array' => false,
]),
],
);
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::equal('attr', ['v'])), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(0)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(100)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(0)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5000)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5001)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('dne', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderAsc('attr')), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('attr')), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(new Query(Query::TYPE_CURSORAFTER, values: ['asdf'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(new Query(Query::TYPE_CURSORBEFORE, values: ['asdf'])), false, $this->validator->getDescription());
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class LimitTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Limit();
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::limit(1)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(0)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(100)), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription());
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class OffsetTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Offset();
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::offset(1)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(0)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5000)), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::offset(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5001)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('attr', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderAsc('attr')), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('attr')), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(100)), false, $this->validator->getDescription());
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class OrderTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Order(
attributes: [
new Document([
'key' => 'attr',
'type' => Database::VAR_STRING,
'array' => false,
]),
],
);
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::orderAsc('attr')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderAsc('')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('attr')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('')), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::offset(5001)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('attr', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('dne', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::equal('', ['v'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::orderDesc('dne')), false, $this->validator->getDescription());
}
}