diff --git a/app/config/collections.php b/app/config/collections.php index 36f6761677..672e2b01ff 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -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], + ], ], ], @@ -1326,6 +1354,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, @@ -1340,6 +1375,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, @@ -1779,6 +1849,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], + ], ], ], @@ -1927,6 +2011,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], + ], ], ], @@ -2075,7 +2194,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], + ], ], ], @@ -2228,6 +2403,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], + ], ], ], @@ -2500,6 +2703,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], + ], ], ], @@ -2691,6 +2922,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], + ], ] ], @@ -3093,6 +3366,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], + ], ] ], ]; diff --git a/app/config/providers.php b/app/config/providers.php index 205d07123b..98ad93b30c 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -121,6 +121,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], + 'etsy' => [ + 'name' => 'Etsy', + 'developers' => 'https://developers.etsy.com/', + 'icon' => 'icon-etsy', + 'enabled' => true, + 'sandbox' => false, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'facebook' => [ 'name' => 'Facebook', 'developers' => 'https://developers.facebook.com/', diff --git a/app/config/roles.php b/app/config/roles.php index 01d1a1858f..80f8bbd459 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -59,9 +59,12 @@ return [ 'home', 'console', 'documents.read', + 'documents.write', 'files.read', + 'files.write', 'locale.read', 'avatars.read', + 'execution.write', ], ], Auth::USER_ROLE_USERS => [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index aa5cc1b2d3..1309eade74 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -18,6 +18,9 @@ use Appwrite\OpenSSL\OpenSSL; 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; @@ -1345,14 +1348,18 @@ 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') - ->action(function (int $limit, int $offset, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) { + ->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) { + + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; $audit = new EventAudit($dbForProject); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 5a125dd480..e530f2568d 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -24,7 +24,6 @@ use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Permissions; -use Utopia\Database\Validator\Query as QueryValidator; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; use Utopia\Database\Exception\Authorization as AuthorizationException; @@ -37,11 +36,16 @@ use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\IP; use Appwrite\Network\Validator\URL; use Appwrite\Utopia\Database\Validator\CustomId; -use Appwrite\Utopia\Database\Validator\Queries as QueriesValidator; +use Appwrite\Utopia\Database\Validator\Query\Limit; +use Appwrite\Utopia\Database\Validator\Query\Offset; use Appwrite\Utopia\Response; use Appwrite\Detector\Detector; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Utopia\Database\Validator\Queries; +use Appwrite\Utopia\Database\Validator\Queries\Collections; +use Appwrite\Utopia\Database\Validator\Queries\Databases; +use Appwrite\Utopia\Database\Validator\Queries\Documents; use Utopia\Config\Config; use MaxMind\Db\Reader; @@ -229,38 +233,36 @@ App::get('/v1/databases') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DATABASE_LIST) + ->param('queries', [], new Databases(), '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(', ', Databases::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 collection 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 collection used as the starting point for the query, excluding the collection 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') - ->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 === 'ASC' ? Query::orderAsc('') : Query::orderDesc(''); - if (!empty($cursor)) { - $cursorDocument = $dbForProject->getDocument('databases', $cursor); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @var Query $cursor */ + $databaseId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('databases', $databaseId); if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Collection '{$cursor}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Database '{$databaseId}' 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([ - 'databases' => $dbForProject->find('databases', \array_merge($filterQueries, $queries)), + 'databases' => $dbForProject->find('databases', $queries), 'total' => $dbForProject->count('databases', $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_DATABASE_LIST); }); @@ -292,24 +294,23 @@ App::get('/v1/databases/:databaseId') }); App::get('/v1/databases/:databaseId/logs') - ->desc('List Collection Logs') + ->desc('List Database Logs') ->groups(['api', 'database']) - ->label('scope', 'collections.read') + ->label('scope', 'databases.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'listLogs') - ->label('sdk.description', '/docs/references/databases/get-collection-logs.md') + ->label('sdk.description', '/docs/references/databases/get-logs.md') ->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('databaseId', '', new UID(), 'Database 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 (string $databaseId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->action(function (string $databaseId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -317,9 +318,17 @@ App::get('/v1/databases/:databaseId/logs') throw new Exception(Exception::DATABASE_NOT_FOUND); } + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId; $logs = $audit->getLogsByResource($resource, $limit, $offset); + + $output = []; + foreach ($logs as $i => &$log) { $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; @@ -538,15 +547,11 @@ App::get('/v1/databases/:databaseId/collections') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION_LIST) ->param('databaseId', '', new UID(), 'Database ID.') + ->param('queries', [], new Collections(), '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(', ', Collections::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 collection 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 collection used as the starting point for the query, excluding the collection 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') - ->action(function (string $databaseId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) { + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -554,30 +559,30 @@ App::get('/v1/databases/:databaseId/collections') throw new Exception(Exception::DATABASE_NOT_FOUND); } - $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 === 'ASC' ? Query::orderAsc('') : Query::orderDesc(''); - if (!empty($cursor)) { - $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $cursor); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @var Query $cursor */ + $collectionId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Collection '{$cursor}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Collection '{$collectionId}' 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([ - 'collections' => $dbForProject->find('database_' . $database->getInternalId(), \array_merge($filterQueries, $queries)), + 'collections' => $dbForProject->find('database_' . $database->getInternalId(), $queries), 'total' => $dbForProject->count('database_' . $database->getInternalId(), $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_COLLECTION_LIST); }); @@ -633,13 +638,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection 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 (string $databaseId, string $collectionId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -653,6 +657,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') throw new Exception(Exception::COLLECTION_NOT_FOUND); } + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId . '/collection/' . $collectionId; $logs = $audit->getLogsByResource($resource, $limit, $offset); @@ -1802,6 +1811,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('usage.metric', 'documents.{scope}.requests.create') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) + ->label('abuse-limit', 120) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createDocument') @@ -1931,17 +1942,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST) ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/database#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) - ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. By default will return maximum 25 results. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' 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 document used as the starting point for the query, excluding the document 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('orderAttributes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes used to sort results. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' order attributes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) - ->param('orderTypes', [], new ArrayList(new WhiteList([Database::ORDER_DESC, Database::ORDER_ASC], true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of order directions for sorting attributes. Possible values are DESC for descending order or ASC for ascending order. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' order types are allowed.', true) + ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true) ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, array $queries, int $limit, int $offset, string $cursor, string $cursorDirection, array $orderAttributes, array $orderTypes, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -1964,52 +1969,41 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::USER_UNAUTHORIZED); } - $filterQueries = \array_map(function ($query) { - $query = Query::parse($query); - - if (\count($query->getValues()) > 100) { - throw new Exception(Exception::GENERAL_QUERY_LIMIT_EXCEEDED, "You cannot use more than 100 query values on attribute '{$query->getAttribute()}'"); - } - - return $query; - }, $queries); - - $otherQueries = []; - $otherQueries[] = Query::limit($limit); - $otherQueries[] = Query::offset($offset); - foreach ($orderTypes as $i => $orderType) { - $otherQueries[] = $orderType === Database::ORDER_DESC ? Query::orderDesc($orderAttributes[$i] ?? '') : Query::orderAsc($orderAttributes[$i] ?? ''); + // Validate queries + $queriesValidator = new Documents($collection->getAttribute('attributes'), $collection->getAttribute('indexes')); + $validQueries = $queriesValidator->isValid($queries); + if (!$validQueries) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription()); } - if (!empty($cursor)) { + $queries = Query::parseQueries($queries); + + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @var Query $cursor */ + $documentId = $cursor->getValue(); + if ($documentSecurity && !$valid) { - $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor); + $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); } else { - $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor)); + $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); } if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$cursor}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$documentId}' for the 'cursor' value not found."); } - $otherQueries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument); + $cursor->setValue($cursorDocument); } - $allQueries = \array_merge($filterQueries, $otherQueries); - - if (!empty($allQueries)) { - $attributes = $collection->getAttribute('attributes', []); - $validator = new QueriesValidator(new QueryValidator($attributes), $attributes, $collection->getAttribute('indexes', []), true); - if (!$validator->isValid($allQueries)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - } + $filterQueries = Query::groupByType($queries)['filters']; if ($documentSecurity && !$valid) { - $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries); + $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries); $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { - $documents = Authorization::skip(fn () => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries)); + $documents = Authorization::skip(fn () => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries)); $total = Authorization::skip(fn () => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT)); } @@ -2102,13 +2096,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('documentId', null, new UID(), 'Document 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 (string $databaseId, string $collectionId, string $documentId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -2128,6 +2121,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen throw new Exception(Exception::DOCUMENT_NOT_FOUND); } + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $resource = 'database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId(); $logs = $audit->getLogsByResource($resource, $limit, $offset); @@ -2191,6 +2189,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}') ->label('usage.metric', 'documents.{scope}.requests.update') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) + ->label('abuse-limit', 60) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'updateDocument') @@ -2320,6 +2320,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}') ->label('usage.metric', 'documents.{scope}.requests.delete') ->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}']) + ->label('abuse-limit', 60) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteDocument') diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 6ea17eb195..29a30ec54b 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -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,36 @@ 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); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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); }); @@ -768,15 +769,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); @@ -784,30 +781,33 @@ 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 resource queries + $queries[] = Query::equal('resourceId', [$function->getId()]); + $queries[] = Query::equal('resourceType', ['functions']); - $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); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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) { @@ -1123,14 +1123,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)); @@ -1138,29 +1135,32 @@ 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 internal queries + $queries[] = Query::equal('functionId', [$function->getId()]); + + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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(); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 398aabf30e..79b9b4752d 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -25,6 +25,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; @@ -150,38 +152,36 @@ 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') - ->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('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); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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']; + $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); }); @@ -326,6 +326,8 @@ App::post('/v1/storage/buckets/:bucketId/files') ->label('audits.resource', 'files/{response.$id}') ->label('usage.metric', 'files.{scope}.requests.create') ->label('usage.params', ['bucketId:{request.bucketId}']) + ->label('abuse-limit', 60) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'createFile') @@ -649,16 +651,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('mode') - ->action(function (string $bucketId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -673,35 +671,38 @@ 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)) { + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @var Query $cursor */ + $fileId = $cursor->getValue(); + if ($fileSecurity && !$valid) { - $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor); + $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor)); + $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$cursor}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$fileId}' 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']; + 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)); } @@ -1204,6 +1205,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->label('audits.resource', 'files/{response.$id}') ->label('usage.metric', 'files.{scope}.requests.update') ->label('usage.params', ['bucketId:{request.bucketId}']) + ->label('abuse-limit', 60) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'updateFile') @@ -1303,6 +1306,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->label('audits.resource', 'file/{request.fileId}') ->label('usage.metric', 'files.{scope}.requests.delete') ->label('usage.params', ['bucketId:{request.bucketId}']) + ->label('abuse-limit', 60) + ->label('abuse-time', 60) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'deleteFile') @@ -1359,9 +1364,12 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->setResource('file/' . $fileId) ; - // Don't need to check valid here because we already ensured validity - if ($fileSecurity) { - $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + try { + $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } } else { $deleted = Authorization::skip(fn() => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); } diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index d0c9f6901b..485e4e0dc3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -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; @@ -74,6 +79,10 @@ App::post('/v1/teams') ]))); if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode + if (!\in_array('owner', $roles)) { + $roles[] = 'owner'; + } + $membershipId = ID::unique(); $membership = new Document([ '$id' => $membershipId, @@ -122,37 +131,35 @@ 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); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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 +467,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 +479,34 @@ 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 internal queries + $queries[] = Query::equal('teamId', [$teamId]); + + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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 +855,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 +868,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'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $resource = 'team/' . $team->getId(); $logs = $audit->getLogsByResource($resource, $limit, $offset); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 63978e5646..04af50ceb0 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -8,6 +8,10 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Network\Validator\Email; 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; @@ -27,7 +31,6 @@ 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; @@ -340,38 +343,36 @@ 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') - ->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('users', $cursor); + // Get cursor document if there was a cursor query + $cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE)); + if ($cursor) { + /** @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']; + $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); }); @@ -525,13 +526,12 @@ 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') - ->action(function (string $userId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { $user = $dbForProject->getDocument('users', $userId); @@ -539,6 +539,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'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + $audit = new Audit($dbForProject); $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); @@ -621,8 +626,7 @@ App::patch('/v1/users/:userId/status') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status)); $events - ->setParam('userId', $user->getId()) - ; + ->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -657,8 +661,7 @@ App::patch('/v1/users/:userId/verification') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); $events - ->setParam('userId', $user->getId()) - ; + ->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -693,8 +696,7 @@ App::patch('/v1/users/:userId/verification/phone') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification)); $events - ->setParam('userId', $user->getId()) - ; + ->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -813,8 +815,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); @@ -935,8 +936,7 @@ App::patch('/v1/users/:userId/prefs') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); $events - ->setParam('userId', $user->getId()) - ; + ->setParam('userId', $user->getId()); $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); @@ -978,8 +978,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId') $events ->setParam('userId', $user->getId()) - ->setParam('sessionId', $sessionId) - ; + ->setParam('sessionId', $sessionId); $response->noContent(); }); @@ -1011,7 +1010,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 } @@ -1020,8 +1020,7 @@ App::delete('/v1/users/:userId/sessions') $events ->setParam('userId', $user->getId()) - ->setPayload($response->output($user, Response::MODEL_USER)) - ; + ->setPayload($response->output($user, Response::MODEL_USER)); $response->noContent(); }); @@ -1059,13 +1058,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)) - ; + ->setPayload($response->output($clone, Response::MODEL_USER)); $response->noContent(); }); @@ -1081,7 +1078,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') diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 480522a254..864ed535f2 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -294,6 +294,7 @@ App::get('/console/databases/collection') $permissions ->setParam('method', 'databases.getCollection') ->setParam('events', 'load,databases.updateCollection') + ->setParam('form', 'collectionPermissions') ->setParam('data', 'project-collection') ->setParam('params', [ 'collection-id' => '{{router.params.id}}', @@ -329,7 +330,6 @@ App::get('/console/databases/document') ->action(function (string $databaseId, string $collection, View $layout) { $logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml'); - $logs ->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0)) ->setParam('method', 'databases.listDocumentLogs') @@ -341,15 +341,16 @@ App::get('/console/databases/document') ; $permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml'); - $permissions ->setParam('method', 'databases.getDocument') ->setParam('events', 'load,databases.updateDocument') + ->setParam('form', 'documentPermissions') ->setParam('data', 'project-document') - ->setParam('permissions', \array_filter( - Database::PERMISSIONS, - fn ($perm) => $perm != Database::PERMISSION_CREATE - )) + ->setParam('permissions', [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]) ->setParam('params', [ 'collection-id' => '{{router.params.collection}}', 'database-id' => '{{router.params.databaseId}}', @@ -384,10 +385,12 @@ App::get('/console/databases/document/new') $permissions ->setParam('data', 'project-document') - ->setParam('permissions', \array_filter( - Database::PERMISSIONS, - fn ($perm) => $perm != Database::PERMISSION_CREATE - )) + ->setParam('form', 'documentPermissions') + ->setParam('permissions', [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]) ->setParam('params', [ 'collection-id' => '{{router.params.collection}}', 'database-id' => '{{router.params.databaseId}}', @@ -451,20 +454,22 @@ App::get('/console/storage/bucket') $fileCreatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml'); $fileCreatePermissions ->setParam('form', 'fileCreatePermissions') - ->setParam('permissions', \array_filter( - Database::PERMISSIONS, - fn ($perm) => $perm != Database::PERMISSION_CREATE - )); + ->setParam('permissions', [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]); $fileUpdatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml'); $fileUpdatePermissions ->setParam('method', 'storage.getFile') ->setParam('data', 'file') ->setParam('form', 'fileUpdatePermissions') - ->setParam('permissions', \array_filter( - Database::PERMISSIONS, - fn ($perm) => $perm != Database::PERMISSION_CREATE - )) + ->setParam('permissions', [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]) ->setParam('params', [ 'bucket-id' => '{{router.params.id}}', ]); diff --git a/app/views/console/comps/permissions-matrix.phtml b/app/views/console/comps/permissions-matrix.phtml index 09005bbb28..d3d937da7f 100644 --- a/app/views/console/comps/permissions-matrix.phtml +++ b/app/views/console/comps/permissions-matrix.phtml @@ -7,7 +7,7 @@ $params = $this->getParam('params', []); $events = $this->getParam('events', ''); $permissions = $this->getParam('permissions', Database::PERMISSIONS); $data = $this->getParam('data', ''); -$form = $this->getParam('form', 'form'); +$form = $this->getParam('form'); $escapedPermissions = \array_map(function ($perm) { // Alpine won't bind to a parameter named delete @@ -34,7 +34,7 @@ $escapedPermissions = \array_map(function ($perm) { data-name="" - @reset.window="permissions = rawPermissions = []"> + @reset.window="permissions.length = rawPermissions.length = 0"> + @addrow.window="addPermission('', role, { })">