Merge pull request #3689 from appwrite/feat-list-users-queries

This commit is contained in:
Christy Jacob 2022-08-30 13:19:56 +04:00 committed by GitHub
commit 177354407a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1707 additions and 134 deletions

View file

@ -1297,6 +1297,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 +1318,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,

View file

@ -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,7 +36,12 @@ 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\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Cursor as CursorQueryValidator;
use Appwrite\Utopia\Database\Validator\Query\Filter as FilterQueryValidator;
use Appwrite\Utopia\Database\Validator\Query\Limit as LimitQueryValidator;
use Appwrite\Utopia\Database\Validator\Query\Offset as OffsetQueryValidator;
use Appwrite\Utopia\Database\Validator\Query\Order as OrderQueryValidator;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Database as EventDatabase;
@ -1964,15 +1968,42 @@ 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()}'");
if (!empty($queries)) {
$attributes = array_merge(
$collection->getAttribute('attributes', []),
[
new Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]),
new Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]),
new Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]),
]
);
$validator = new IndexedQueries(
$attributes,
$collection->getAttribute('indexes', []),
new CursorQueryValidator(),
new FilterQueryValidator($attributes),
new LimitQueryValidator(),
new OffsetQueryValidator(),
new OrderQueryValidator(),
);
if (!$validator->isValid($queries)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
}
return $query;
}, $queries);
$filterQueries = Query::parseQueries($queries);
$otherQueries = [];
$otherQueries[] = Query::limit($limit);
@ -1997,14 +2028,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$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());
}
}
if ($documentSecurity && !$valid) {
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT);

View file

@ -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,39 @@ 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);
// 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'];
$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 +529,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 +542,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);
@ -621,8 +629,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 +664,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 +699,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 +818,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 +939,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 +981,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $sessionId)
;
->setParam('sessionId', $sessionId);
$response->noContent();
});
@ -1011,7 +1013,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 +1023,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 +1061,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 +1081,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')

76
composer.lock generated
View file

@ -481,16 +481,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.5",
"version": "7.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82"
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"shasum": ""
},
"require": {
@ -505,10 +505,10 @@
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"ext-curl": "*",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"phpunit/phpunit": "^8.5.29 || ^9.5.23",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@ -518,8 +518,12 @@
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "7.4-dev"
"dev-master": "7.5-dev"
}
},
"autoload": {
@ -585,7 +589,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.5"
"source": "https://github.com/guzzle/guzzle/tree/7.5.0"
},
"funding": [
{
@ -601,20 +605,20 @@
"type": "tidelift"
}
],
"time": "2022-06-20T22:16:13+00:00"
"time": "2022-08-28T15:39:27+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.5.1",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da"
"reference": "b94b2807d85443f9719887892882d0329d1e2598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598",
"reference": "b94b2807d85443f9719887892882d0329d1e2598",
"shasum": ""
},
"require": {
@ -669,7 +673,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.5.1"
"source": "https://github.com/guzzle/promises/tree/1.5.2"
},
"funding": [
{
@ -685,20 +689,20 @@
"type": "tidelift"
}
],
"time": "2021-10-22T20:56:57+00:00"
"time": "2022-08-28T14:55:35+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.4.0",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "13388f00956b1503577598873fffb5ae994b5737"
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737",
"reference": "13388f00956b1503577598873fffb5ae994b5737",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379",
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379",
"shasum": ""
},
"require": {
@ -712,15 +716,19 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.8 || ^9.3.10"
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "2.4-dev"
}
@ -784,7 +792,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.4.0"
"source": "https://github.com/guzzle/psr7/tree/2.4.1"
},
"funding": [
{
@ -800,7 +808,7 @@
"type": "tidelift"
}
],
"time": "2022-06-20T21:43:11+00:00"
"time": "2022-08-28T14:45:39+00:00"
},
{
"name": "influxdb/influxdb-php",
@ -2833,12 +2841,12 @@
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "1a67d9dcd2884a6a708176955f83e319ac53059e"
"reference": "6e630a62f522ac68a7056bebf81cd032c7a053ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1a67d9dcd2884a6a708176955f83e319ac53059e",
"reference": "1a67d9dcd2884a6a708176955f83e319ac53059e",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6e630a62f522ac68a7056bebf81cd032c7a053ba",
"reference": "6e630a62f522ac68a7056bebf81cd032c7a053ba",
"shasum": ""
},
"require": {
@ -2876,7 +2884,7 @@
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/master"
},
"time": "2022-08-19T10:03:22+00:00"
"time": "2022-08-29T10:43:33+00:00"
},
{
"name": "doctrine/instantiator",
@ -4802,16 +4810,16 @@
},
{
"name": "sebastian/type",
"version": "3.0.0",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad"
"reference": "fb44e1cc6e557418387ad815780360057e40753e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb44e1cc6e557418387ad815780360057e40753e",
"reference": "fb44e1cc6e557418387ad815780360057e40753e",
"shasum": ""
},
"require": {
@ -4823,7 +4831,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.1-dev"
}
},
"autoload": {
@ -4846,7 +4854,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/3.0.0"
"source": "https://github.com/sebastianbergmann/type/tree/3.1.0"
},
"funding": [
{
@ -4854,7 +4862,7 @@
"type": "github"
}
],
"time": "2022-03-15T09:54:48+00:00"
"time": "2022-08-29T06:55:37+00:00"
},
{
"name": "sebastian/version",

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,140 @@
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,86 @@
<?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\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
class Collection extends IndexedQueries
{
/**
* 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([
'$id' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new Document([
'$id' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$indexes = [];
foreach ($allowedAttributes as $attribute) {
$indexes[] = new Document([
'status' => 'available',
'type' => Database::INDEX_KEY,
'attributes' => [$attribute]
]);
}
$indexes[] = new Document([
'status' => 'available',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search']
]);
$validators = [
new Limit(),
new Offset(),
new Cursor(),
new Filter($attributes),
new Order($attributes),
];
parent::__construct($attributes, $indexes, ...$validators);
}
}

View file

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

View file

@ -0,0 +1,61 @@
<?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;
}
/**
* 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

@ -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

@ -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

@ -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,43 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Collection;
use PHPUnit\Framework\TestCase;
class CollectionTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testEmptyQueries(): void
{
$validator = new Collection('users', []);
$this->assertEquals($validator->isValid([]), true);
}
public function testValid(): void
{
$validator = new Collection('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());
$this->assertEquals(true, $validator->isValid(['search("search", "value")']), $validator->getDescription());
}
public function testMissingIndex(): void
{
$validator = new Collection('users', ['name']);
$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("search", "value")']), $validator->getDescription());
}
}

View file

@ -0,0 +1,40 @@
<?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());
$this->assertEquals(true, $validator->isValid(['search("search", "value")']), $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(100);
}
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(5000);
}
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,55 @@
<?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());
$this->assertEquals($this->validator->isValid(Query::orderAsc('dne')), false, $this->validator->getDescription());
}
}