1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00

Merge pull request #3699 from appwrite/feat-list-user-logs-queries

Update List User Logs API to Accept Queries
This commit is contained in:
Steven 2022-08-22 13:18:56 -07:00 committed by GitHub
commit ad417731ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1372 additions and 79 deletions

View file

@ -25,7 +25,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;
@ -38,8 +37,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\OrderAttributes;
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;
@ -2028,15 +2031,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);
@ -2060,14 +2090,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) {
$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

@ -9,7 +9,10 @@ use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
@ -29,7 +32,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;
@ -573,14 +575,13 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('usage')
->action(function (string $userId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) {
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
@ -588,6 +589,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);

16
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5d5b72c7940a8376e187cce1b33d327a",
"content-hash": "0e850206a924d2a48861ecd290f59bc0",
"packages": [
{
"name": "adhocore/jwt",
@ -3526,23 +3526,23 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.15",
"version": "9.2.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
"reference": "2593003befdcc10db5e213f9f28814f5aa8ac073"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073",
"reference": "2593003befdcc10db5e213f9f28814f5aa8ac073",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0",
"nikic/php-parser": "^4.14",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -3591,7 +3591,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16"
},
"funding": [
{
@ -3599,7 +3599,7 @@
"type": "github"
}
],
"time": "2022-03-07T09:28:20+00:00"
"time": "2022-08-20T05:26:47+00:00"
},
{
"name": "phpunit/php-file-iterator",

View file

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

View file

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

View file

@ -2,13 +2,17 @@
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;
use Utopia\Database\Validator\Queries as QueriesValidator;
use Utopia\Database\Validator\Query as QueryValidator;
class Collection extends QueriesValidator
class Collection extends IndexedQueries
{
/**
* Expression constructor
@ -39,6 +43,22 @@ class Collection extends QueriesValidator
]);
}
$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([
@ -53,6 +73,14 @@ class Collection extends QueriesValidator
'attributes' => ['search']
]);
parent::__construct(new QueryValidator($attributes), $attributes, $indexes, true);
$validators = [
new Limit(),
new Offset(),
new Cursor(),
new Filter($attributes),
new Order($attributes),
];
parent::__construct($attributes, $indexes, ...$validators);
}
}

View file

@ -15,6 +15,7 @@ class Users extends Collection
'registration',
'emailVerification',
'phoneVerification',
'search',
];
/**

View file

@ -1,21 +0,0 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Validator\UID;
use Utopia\Database\Validator\Query as QueryValidator;
class Query extends QueryValidator
{
protected function isValidCursor($cursor): bool
{
$validator = new UID();
if ($validator->isValid($cursor)) {
return true;
}
$this->message = 'Invalid cursor: ' . $validator->getDescription();
return false;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
namespace Tests\E2E\Services\Users;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\ID;
trait UsersBase
@ -997,7 +996,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)']
'offset' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
@ -1008,16 +1007,80 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
'offset(1)',
'limit(1)',
]
'offset' => 1,
'limit' => 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();
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::limit(1)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(0)), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(100)), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(-1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(101)), false, $this->validator->getDescription());
}
}

View file

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

View file

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