diff --git a/composer.lock b/composer.lock index ff525b0a8..05ea6441e 100644 --- a/composer.lock +++ b/composer.lock @@ -336,23 +336,24 @@ }, { "name": "utopia-php/framework", - "version": "0.28.1", + "version": "0.28.2", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "7f22c556fc5991e54e5811a68fb39809b21bda55" + "reference": "bc0144ff3983afa6724c43f2ce578fdbceec21f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/7f22c556fc5991e54e5811a68fb39809b21bda55", - "reference": "7f22c556fc5991e54e5811a68fb39809b21bda55", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/bc0144ff3983afa6724c43f2ce578fdbceec21f9", + "reference": "bc0144ff3983afa6724c43f2ce578fdbceec21f9", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=8.0" }, "require-dev": { "laravel/pint": "^1.2", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.5.25", "vimeo/psalm": "4.27.0" }, @@ -374,9 +375,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.28.1" + "source": "https://github.com/utopia-php/framework/tree/0.28.2" }, - "time": "2023-03-02T08:16:01+00:00" + "time": "2023-05-30T06:47:57+00:00" }, { "name": "utopia-php/mongo", diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index a676f9acb..df9481005 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1178,6 +1178,7 @@ public function sum(string $collection, string $attribute, array $queries = [], * @param array $selections * @param string $prefix * @return mixed + * @throws Exception */ protected function getAttributeProjection(array $selections, string $prefix = ''): mixed { @@ -1196,11 +1197,11 @@ protected function getAttributeProjection(array $selections, string $prefix = '' if (!empty($prefix)) { foreach ($selections as &$selection) { - $selection = "`{$prefix}`.`{$selection}`"; + $selection = "`{$prefix}`.`{$this->filter($selection)}`"; } } else { foreach ($selections as &$selection) { - $selection = "`{$selection}`"; + $selection = "`{$this->filter($selection)}`"; } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index fb569c85f..18e906200 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1185,6 +1185,7 @@ public function sum(string $collection, string $attribute, array $queries = [], * @param string[] $selections * @param string $prefix * @return string + * @throws Exception */ protected function getAttributeProjection(array $selections, string $prefix = ''): string { @@ -1203,11 +1204,11 @@ protected function getAttributeProjection(array $selections, string $prefix = '' if (!empty($prefix)) { foreach ($selections as &$selection) { - $selection = "\"{$prefix}\".\"{$selection}\""; + $selection = "\"{$prefix}\".\"{$this->filter($selection)}\""; } } else { foreach ($selections as &$selection) { - $selection = "\"{$selection}\""; + $selection = "\"{$this->filter($selection)}\""; } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 053b41d39..81bb24ade 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -16,6 +16,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Queries\Documents; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Structure; @@ -3838,6 +3839,13 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu } $collection = $this->silent(fn () => $this->getCollection($collection)); + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + $validator = new Documents($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new Exception($validator->getDescription()); + } $authorization = new Authorization(self::PERMISSION_READ); $skipAuth = $authorization->isValid($collection->getRead()); diff --git a/src/Database/Query.php b/src/Database/Query.php index a7fafa7b5..60ada7820 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -698,11 +698,10 @@ public static function endsWith(string $attribute, string $value): self * Filters $queries for $types * * @param array $queries - * @param string ...$types - * + * @param array $types * @return array */ - public static function getByType(array $queries, string ...$types): array + public static function getByType(array $queries, array $types): array { $filtered = []; foreach ($queries as $query) { diff --git a/src/Database/Validator/IndexedQueries.php b/src/Database/Validator/IndexedQueries.php new file mode 100644 index 000000000..24846460e --- /dev/null +++ b/src/Database/Validator/IndexedQueries.php @@ -0,0 +1,103 @@ + + */ + protected array $attributes = []; + + /** + * @var array + */ + protected array $indexes = []; + + /** + * Expression constructor + * + * This Queries Validator filters indexes for only available indexes + * + * @param array $attributes + * @param array $indexes + * @param array $validators + * @throws Exception + */ + public function __construct(array $attributes = [], array $indexes = [], array $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); + } + + /** + * @param mixed $value + * @return bool + * @throws Exception + */ + 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); + $filters = $grouped['filters']; + + foreach ($filters as $filter) { + if ($filter->getMethod() === Query::TYPE_SEARCH) { + $matched = false; + + foreach ($this->indexes as $index) { + if ( + $index->getAttribute('type') === Database::INDEX_FULLTEXT + && $index->getAttribute('attributes') === [$filter->getAttribute()] + ) { + $matched = true; + } + } + + if (!$matched) { + $this->message = "Searching by attribute \"{$filter->getAttribute()}\" requires a fulltext index."; + return false; + } + } + } + + return true; + } +} diff --git a/src/Database/Validator/OrderAttributes.php b/src/Database/Validator/OrderAttributes.php deleted file mode 100644 index b4bb1f45b..000000000 --- a/src/Database/Validator/OrderAttributes.php +++ /dev/null @@ -1,192 +0,0 @@ -> - */ - protected array $schema = []; - - /** - * @var array> - */ - protected array $indexes = []; - - /** - * @var bool - */ - protected bool $strict; - - /** - * Expression constructor - * - * @param array $attributes - * @param array $indexes - * @param bool $strict - */ - public function __construct(array $attributes, array $indexes, bool $strict = true) - { - $this->schema[] = [ - 'key' => '$id', - 'array' => false, - 'type' => Database::VAR_STRING, - 'size' => 512 - ]; - - $this->schema[] = [ - 'key' => '$createdAt', - 'array' => false, - 'type' => Database::VAR_INTEGER, - 'size' => 0 - ]; - - $this->schema[] = [ - 'key' => '$updatedAt', - 'array' => false, - 'type' => Database::VAR_INTEGER, - 'size' => 0 - ]; - - foreach ($attributes as $attribute) { - $this->schema[] = $attribute->getArrayCopy(); - } - - $this->indexes[] = [ - 'type' => Database::INDEX_UNIQUE, - 'attributes' => ['$id'] - ]; - - $this->indexes[] = [ - 'type' => Database::INDEX_KEY, - 'attributes' => ['$createdAt'] - ]; - - $this->indexes[] = [ - 'type' => Database::INDEX_KEY, - 'attributes' => ['$updatedAt'] - ]; - - foreach ($indexes as $index) { - $this->indexes[] = $index->getArrayCopy(['attributes', 'type']); - } - - $this->strict = $strict; - } - - /** - * Get Description. - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return $this->message; - } - - /** - * Is valid. - * - * Returns true if query typed according to schema. - * - * @param mixed $value - * - * @return bool - */ - public function isValid($value): bool - { - foreach ($value as $attribute) { - // Search for attribute in schema - $attributeInSchema = \in_array($attribute, \array_column($this->schema, 'key')); - - if ($attributeInSchema === false) { - $this->message = 'Order attribute not found in schema: ' . $attribute; - return false; - } - } - - $found = null; - - // Return false if attributes do not exactly match an index - if ($this->strict) { - // look for strict match among indexes - foreach ($this->indexes as $index) { - if ($this->arrayMatch($index['attributes'], $value)) { - $found = $index; - } - } - - if (!$found) { - $this->message = 'Index not found: ' . implode(",", $value); - return false; - } - - // search method requires fulltext index - if (in_array(Query::TYPE_SEARCH, $value) && $found['type'] !== Database::INDEX_FULLTEXT) { - $this->message = 'Search method requires fulltext index: ' . implode(",", $value); - return false; - } - } - - return true; - } - /** - * 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; - } - - /** - * 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($indexes) !== count($queries)) { - return false; - } - - // 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; - } -} diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index b5b496c17..c80a18366 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -2,64 +2,30 @@ namespace Utopia\Database\Validator; -use Exception; -use Utopia\Database\Database; -use Utopia\Database\Document; -use Utopia\Database\Query; -use Utopia\Database\Validator\Query as QueryValidator; +use Utopia\Database\Validator\Query\Base; use Utopia\Validator; +use Utopia\Database\Query; class Queries extends Validator { - protected string $message = 'Invalid queries'; - - protected QueryValidator $validator; - /** - * @var array + * @var string */ - protected array $attributes = []; + protected string $message = 'Invalid queries'; /** - * @var array + * @var array */ - protected array $indexes = []; - protected bool $strict; + protected array $validators; /** * Queries constructor * - * @param QueryValidator $validator used to validate each query - * @param array|null $attributes allowed attributes to be queried - * @param array|null $indexes available for strict query matching - * @param bool $strict - * @throws Exception + * @param array $validators */ - public function __construct(QueryValidator $validator, ?array $attributes, ?array $indexes, bool $strict = true) + public function __construct(array $validators = []) { - $this->validator = $validator; - $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; - } - - $this->strict = $strict; + $this->validators = $validators; } /** @@ -75,85 +41,70 @@ public function getDescription(): string } /** - * Is valid. - * - * Returns false if: - * 1. any query in $value is invalid based on $validator - * - * In addition, if $strict is true, this returns false if: - * 1. there is no index with an exact match of the filters - * 2. there is no index with an exact match of the order attributes - * - * Otherwise, returns true. - * - * @param mixed $value + * @param array $value * @return bool */ public function isValid($value): bool { - $queries = []; + if (!is_array($value)) { + $this->message = 'Queries must be an array'; + return false; + } + foreach ($value as $query) { if (!$query instanceof Query) { try { $query = Query::parse($query); - } catch (\Throwable $th) { - $this->message = 'Invalid query: ${query}'; + } catch (\Throwable $e) { + $this->message = 'Invalid query: ' . $e->getMessage(); return false; } } - if (!$this->validator->isValid($query)) { - $this->message = 'Query not valid: ' . $this->validator->getDescription(); - return false; - } - - $queries[] = $query; - } - - if (!$this->strict) { - return true; - } - - $grouped = Query::groupByType($queries); - /** @var array $filters */ - $filters = $grouped['filters']; - /** @var array $orderAttributes */ - $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; + $method = $query->getMethod(); + $methodType = match ($method) { + Query::TYPE_SELECT => Base::METHOD_TYPE_SELECT, + Query::TYPE_LIMIT => Base::METHOD_TYPE_LIMIT, + Query::TYPE_OFFSET => Base::METHOD_TYPE_OFFSET, + Query::TYPE_CURSORAFTER, + Query::TYPE_CURSORBEFORE => Base::METHOD_TYPE_CURSOR, + Query::TYPE_ORDERASC, + Query::TYPE_ORDERDESC => Base::METHOD_TYPE_ORDER, + Query::TYPE_EQUAL, + Query::TYPE_NOT_EQUAL, + Query::TYPE_LESSER, + Query::TYPE_LESSER_EQUAL, + Query::TYPE_GREATER, + Query::TYPE_GREATER_EQUAL, + Query::TYPE_SEARCH, + Query::TYPE_IS_NULL, + Query::TYPE_IS_NOT_NULL, + Query::TYPE_BETWEEN, + Query::TYPE_STARTS_WITH, + Query::TYPE_CONTAINS, + Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER, + default => '', + }; + + $methodIsValid = false; + foreach ($this->validators as $validator) { + if ($validator->getMethodType() !== $methodType) { + continue; + } + if (!$validator->isValid($query)) { + $this->message = 'Invalid query: ' . $validator->getDescription(); + return false; } - } - if (!$found) { - $this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute)); - return false; + $methodIsValid = true; } - // 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)); + if (!$methodIsValid) { + $this->message = 'Invalid query method: ' . $method; 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; } @@ -180,43 +131,4 @@ public function getType(): string { return self::TYPE_OBJECT; } - - /** - * Is Strict - * - * Returns true if strict validation is set - * - * @return bool - */ - public function isStrict(): bool - { - return $this->strict; - } - - /** - * 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; - } } diff --git a/src/Database/Validator/Queries/Document.php b/src/Database/Validator/Queries/Document.php new file mode 100644 index 000000000..fbb164d9d --- /dev/null +++ b/src/Database/Validator/Queries/Document.php @@ -0,0 +1,45 @@ + $attributes + * @throws Exception + */ + public function __construct(array $attributes) + { + $attributes[] = new \Utopia\Database\Document([ + '$id' => '$id', + 'key' => '$id', + 'type' => Database::VAR_STRING, + 'array' => false, + ]); + + $attributes[] = new \Utopia\Database\Document([ + '$id' => '$createdAt', + 'key' => '$createdAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); + + $attributes[] = new \Utopia\Database\Document([ + '$id' => '$updatedAt', + 'key' => '$updatedAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); + + $validators = [ + new Select($attributes), + ]; + + parent::__construct($validators); + } +} diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php new file mode 100644 index 000000000..2dd7c0819 --- /dev/null +++ b/src/Database/Validator/Queries/Documents.php @@ -0,0 +1,59 @@ + $attributes + * @param array $indexes + * @throws Exception + */ + public function __construct(array $attributes, array $indexes) + { + $attributes[] = new Document([ + '$id' => '$id', + 'key' => '$id', + 'type' => Database::VAR_STRING, + 'array' => false, + ]); + + $attributes[] = new Document([ + '$id' => '$createdAt', + 'key' => '$createdAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); + + $attributes[] = new Document([ + '$id' => '$updatedAt', + 'key' => '$updatedAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); + + $validators = [ + new Limit(), + new Offset(), + new Cursor(), + new Filter($attributes), + new Order($attributes), + new Select($attributes), + ]; + + parent::__construct($attributes, $indexes, $validators); + } +} diff --git a/src/Database/Validator/Query.php b/src/Database/Validator/Query.php deleted file mode 100644 index 0cd5eda61..000000000 --- a/src/Database/Validator/Query.php +++ /dev/null @@ -1,289 +0,0 @@ -> - */ - protected array $schema = []; - - protected int $maxLimit; - protected int $maxOffset; - protected int $maxValuesCount; - - /** - * Query constructor - * - * @param array $attributes - * @param int $maxLimit - * @param int $maxOffset - * @param int $maxValuesCount - */ - public function __construct(array $attributes, int $maxLimit = 100, int $maxOffset = 5000, int $maxValuesCount = 100) - { - $this->schema['$id'] = [ - 'key' => '$id', - 'array' => false, - 'type' => Database::VAR_STRING, - 'size' => 512 - ]; - - $this->schema['$createdAt'] = [ - 'key' => '$createdAt', - 'array' => false, - 'type' => Database::VAR_DATETIME, - 'size' => 0 - ]; - - $this->schema['$updatedAt'] = [ - 'key' => '$updatedAt', - 'array' => false, - 'type' => Database::VAR_DATETIME, - 'size' => 0 - ]; - - foreach ($attributes as $attribute) { - $this->schema[(string)$attribute->getAttribute('key')] = $attribute->getArrayCopy(); - } - - $this->maxLimit = $maxLimit; - $this->maxOffset = $maxOffset; - $this->maxValuesCount = $maxValuesCount; - } - - /** - * Get Description. - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return $this->message; - } - - protected function isValidLimit(?int $limit): bool - { - $validator = new Range(0, $this->maxLimit); - if ($validator->isValid($limit)) { - return true; - } - - $this->message = 'Invalid limit: ' . $validator->getDescription(); - return false; - } - - protected function isValidOffset(?int $offset): bool - { - $validator = new Range(0, $this->maxOffset); - if ($validator->isValid($offset)) { - return true; - } - - $this->message = 'Invalid offset: ' . $validator->getDescription(); - return false; - } - - protected function isValidCursor(?string $cursor): bool - { - if ($cursor === null) { - $this->message = 'Cursor must not be null'; - return false; - } - return true; - } - - protected function isValidAttribute(string $attribute): bool - { - // Search for attribute in schema - if (!isset($this->schema[$attribute])) { - $this->message = 'Attribute not found in schema: ' . $attribute; - return false; - } - - return true; - } - - /** - * @param string $attribute - * @param array $values - * @return bool - */ - 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) { - switch ($attributeType) { - case Database::VAR_DATETIME: - $condition = gettype($value) === Database::VAR_STRING; - break; - case Database::VAR_FLOAT: - $condition = (gettype($value) === Database::VAR_FLOAT || gettype($value) === Database::VAR_INTEGER); - break; - default: - $condition = gettype($value) === $attributeType; - break; - } - - if (!$condition) { - $this->message = 'Query type does not match expected: ' . $attributeType; - return false; - } - } - - return true; - } - - /** - * @param string $attribute - * @param array $values - * @return bool - */ - protected function isValidContains(string $attribute, array $values): bool - { - if (!$this->isValidAttributeAndValues($attribute, $values)) { - return false; - } - - $attributeSchema = $this->schema[$attribute]; - - // Contains method only supports array attributes - if (!$attributeSchema['array']) { - $this->message = 'Query method only supported on array attributes: ' . DatabaseQuery::TYPE_CONTAINS; - return false; - } - - return true; - } - - /** - * @param array $attributes - * @return bool - */ - protected function isValidSelect(array $attributes): bool - { - foreach ($attributes as $attribute) { - if (!$this->isValidAttribute($attribute)) { - return false; - } - } - - return true; - } - - /** - * Is valid. - * - * Returns false if: - * 1. $query has an invalid method - * 2. limit value is not a number, less than 0, or greater than $maxLimit - * 3. offset value is not a number, less than 0, or greater than $maxOffset - * 4. attribute does not exist - * 5. count of values is greater than $maxValuesCount - * 6. value type does not match attribute type - * 6. contains method is used on non-array attribute - * - * Otherwise, returns true. - * - * @param DatabaseQuery $query - * - * @return bool - */ - public function isValid($query): bool - { - // Validate method - $method = $query->getMethod(); - if (!DatabaseQuery::isMethod($method)) { - $this->message = 'Query method invalid: ' . $method; - return false; - } - - $attribute = $query->getAttribute(); - - switch ($method) { - case DatabaseQuery::TYPE_LIMIT: - $limit = $query->getValue(); - return $this->isValidLimit($limit); - - case DatabaseQuery::TYPE_OFFSET: - $offset = $query->getValue(); - return $this->isValidOffset($offset); - - case DatabaseQuery::TYPE_CURSORAFTER: - case DatabaseQuery::TYPE_CURSORBEFORE: - $cursor = $query->getValue(); - return $this->isValidCursor($cursor); - - case DatabaseQuery::TYPE_ORDERASC: - case DatabaseQuery::TYPE_ORDERDESC: - // Allow empty string for order attribute so we can order by natural order - if ($attribute === '') { - return true; - } - return $this->isValidAttribute($attribute); - - case DatabaseQuery::TYPE_CONTAINS: - $values = $query->getValues(); - return $this->isValidContains($attribute, $values); - - case DatabaseQuery::TYPE_SELECT: - $attributes = $query->getValues(); - return $this->isValidSelect($attributes); - - default: - // other filter queries - $values = $query->getValues(); - return $this->isValidAttributeAndValues($attribute, $values); - } - } - /** - * 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; - } -} diff --git a/src/Database/Validator/Query/Base.php b/src/Database/Validator/Query/Base.php new file mode 100644 index 000000000..a37fdd65a --- /dev/null +++ b/src/Database/Validator/Query/Base.php @@ -0,0 +1,58 @@ +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; +} diff --git a/src/Database/Validator/Query/Cursor.php b/src/Database/Validator/Query/Cursor.php new file mode 100644 index 000000000..b976d8412 --- /dev/null +++ b/src/Database/Validator/Query/Cursor.php @@ -0,0 +1,51 @@ +getMethod(); + + if ($method === Query::TYPE_CURSORAFTER || $method === Query::TYPE_CURSORBEFORE) { + $cursor = $value->getValue(); + + if ($cursor instanceof Document) { + $cursor = $cursor->getId(); + } + + $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; + } +} diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php new file mode 100644 index 000000000..9eab6b28e --- /dev/null +++ b/src/Database/Validator/Query/Filter.php @@ -0,0 +1,184 @@ + + */ + protected array $schema = []; + + private int $maxValuesCount; + + /** + * @param array $attributes + * @param int $maxValuesCount + */ + public function __construct(array $attributes = [], int $maxValuesCount = 100) + { + foreach ($attributes as $attribute) { + $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); + } + + $this->maxValuesCount = $maxValuesCount; + } + + /** + * @param string $attribute + * @return bool + */ + protected function isValidAttribute(string $attribute): bool + { + if (\str_contains($attribute, '.')) { + // Check for special symbol `.` + if (isset($this->schema[$attribute])) { + return true; + } + + // For relationships, just validate the top level. + // will validate each nested level during the recursive calls. + $attribute = \explode('.', $attribute)[0]; + + if (isset($this->schema[$attribute])) { + $this->message = 'Cannot query nested attribute on: ' . $attribute; + return false; + } + } + + // Search for attribute in schema + if (!isset($this->schema[$attribute])) { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + + return true; + } + + /** + * @param string $attribute + * @param array $values + * @return bool + */ + protected function isValidAttributeAndValues(string $attribute, array $values): bool + { + if (!$this->isValidAttribute($attribute)) { + return false; + } + + // isset check if for special symbols "." in the attribute name + if (\str_contains($attribute, '.') && !isset($this->schema[$attribute])) { + // For relationships, just validate the top level. + // Utopia will validate each nested level during the recursive calls. + $attribute = \explode('.', $attribute)[0]; + } + + $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_RELATIONSHIP => true, + Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING, + Database::VAR_FLOAT => (gettype($value) === Database::VAR_FLOAT || gettype($value) === Database::VAR_INTEGER), + default => gettype($value) === $attributeType + }; + + if (!$condition) { + $this->message = 'Query type does not match expected: ' . $attributeType; + return false; + } + } + + return true; + } + + /** + * @param array $values + * @return bool + */ + protected function isEmpty(array $values): bool + { + if (count($values) === 0) { + return true; + } + + if (is_array($values[0]) && count($values[0]) === 0) { + return true; + } + + return false; + } + + /** + * 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($value): bool + { + $method = $value->getMethod(); + $attribute = $value->getAttribute(); + switch ($method) { + case Query::TYPE_EQUAL: + case Query::TYPE_CONTAINS: + if ($this->isEmpty($value->getValues())) { + $this->message = \ucfirst($method) . ' queries require at least one value.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $value->getValues()); + + case Query::TYPE_NOT_EQUAL: + case Query::TYPE_LESSER: + case Query::TYPE_LESSER_EQUAL: + case Query::TYPE_GREATER: + case Query::TYPE_GREATER_EQUAL: + case Query::TYPE_SEARCH: + case Query::TYPE_STARTS_WITH: + case Query::TYPE_ENDS_WITH: + if (count($value->getValues()) != 1) { + $this->message = \ucfirst($method) . ' queries require exactly one value.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $value->getValues()); + + case Query::TYPE_BETWEEN: + if (count($value->getValues()) != 2) { + $this->message = \ucfirst($method) . ' queries require exactly two values.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $value->getValues()); + + case Query::TYPE_IS_NULL: + case Query::TYPE_IS_NOT_NULL: + return $this->isValidAttributeAndValues($attribute, $value->getValues()); + + default: + return false; + } + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_FILTER; + } +} diff --git a/src/Database/Validator/Query/Limit.php b/src/Database/Validator/Query/Limit.php new file mode 100644 index 000000000..facc266d7 --- /dev/null +++ b/src/Database/Validator/Query/Limit.php @@ -0,0 +1,63 @@ +maxLimit = $maxLimit; + } + + /** + * Is valid. + * + * Returns true if method is limit values are within range. + * + * @param Query $value + * @return bool + */ + public function isValid($value): bool + { + if (!$value instanceof Query) { + return false; + } + + if ($value->getMethod() !== Query::TYPE_LIMIT) { + $this->message = 'Invalid query method: ' . $value->getMethod(); + return false; + } + + $limit = $value->getValue(); + + $validator = new Numeric(); + if (!$validator->isValid($limit)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + $validator = new Range(1, $this->maxLimit); + if (!$validator->isValid($limit)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + return true; + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_LIMIT; + } +} diff --git a/src/Database/Validator/Query/Offset.php b/src/Database/Validator/Query/Offset.php new file mode 100644 index 000000000..8d59be4d0 --- /dev/null +++ b/src/Database/Validator/Query/Offset.php @@ -0,0 +1,59 @@ +maxOffset = $maxOffset; + } + + /** + * @param Query $value + * @return bool + */ + public function isValid($value): bool + { + if (!$value instanceof Query) { + return false; + } + + $method = $value->getMethod(); + + if ($method !== Query::TYPE_OFFSET) { + $this->message = 'Query method invalid: ' . $method; + return false; + } + + $offset = $value->getValue(); + + $validator = new Numeric(); + if (!$validator->isValid($offset)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + $validator = new Range(0, $this->maxOffset); + if (!$validator->isValid($offset)) { + $this->message = 'Invalid offset: ' . $validator->getDescription(); + return false; + } + + return true; + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_OFFSET; + } +} diff --git a/src/Database/Validator/Query/Order.php b/src/Database/Validator/Query/Order.php new file mode 100644 index 000000000..40ef707d4 --- /dev/null +++ b/src/Database/Validator/Query/Order.php @@ -0,0 +1,73 @@ + + */ + protected array $schema = []; + + /** + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + foreach ($attributes as $attribute) { + $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); + } + } + + /** + * @param string $attribute + * @return bool + */ + protected function isValidAttribute(string $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($value): bool + { + if (!$value instanceof Query) { + return false; + } + + $method = $value->getMethod(); + $attribute = $value->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; + } +} diff --git a/src/Database/Validator/Query/Select.php b/src/Database/Validator/Query/Select.php new file mode 100644 index 000000000..de20fc138 --- /dev/null +++ b/src/Database/Validator/Query/Select.php @@ -0,0 +1,69 @@ + + */ + protected array $schema = []; + + /** + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + foreach ($attributes as $attribute) { + $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); + } + } + + /** + * Is valid. + * + * Returns true if method is TYPE_SELECT selections are valid + * + * Otherwise, returns false + * + * @param Query $value + * @return bool + */ + public function isValid($value): bool + { + if (!$value instanceof Query) { + return false; + } + + if ($value->getMethod() !== Query::TYPE_SELECT) { + return false; + } + + foreach ($value->getValues() as $attribute) { + if (\str_contains($attribute, '.')) { + //special symbols with `dots` + if (isset($this->schema[$attribute])) { + continue; + } + + // For relationships, just validate the top level. + // Will validate each nested level during the recursive calls. + $attribute = \explode('.', $attribute)[0]; + } + + if (!isset($this->schema[$attribute]) && $attribute !== '*') { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + } + return true; + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_SELECT; + } +} diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 9697fabb7..17a2bbc05 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -21,6 +21,7 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\Database\Validator\Query\Filter; use Utopia\Database\Validator\Structure; use Utopia\Validator\Range; use Utopia\Database\Exception\Structure as StructureException; @@ -270,6 +271,66 @@ public function testAttributeKeyWithSymbols(): void $this->assertEquals('value', $document->getAttribute('key_with.sym$bols')); } + public function testAttributeNamesWithDots(): void + { + static::getDatabase()->createCollection('dots.parent'); + + $this->assertTrue(static::getDatabase()->createAttribute( + collection: 'dots.parent', + id: 'dots.name', + type: Database::VAR_STRING, + size: 255, + required: false + )); + + $document = static::getDatabase()->find('dots.parent', [ + Query::select(['dots.name']), + ]); + $this->assertEmpty($document); + + static::getDatabase()->createCollection('dots'); + + $this->assertTrue(static::getDatabase()->createAttribute( + collection: 'dots', + id: 'name', + type: Database::VAR_STRING, + size: 255, + required: false + )); + + static::getDatabase()->createRelationship( + collection: 'dots.parent', + relatedCollection: 'dots', + type: Database::RELATION_ONE_TO_ONE + ); + + static::getDatabase()->createDocument('dots.parent', new Document([ + '$id' => ID::custom('father'), + 'dots.name' => 'Bill clinton', + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'dots' => [ + '$id' => ID::custom('child'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ] + ])); + + $documents = static::getDatabase()->find('dots.parent', [ + Query::select(['*']), + ]); + + $this->assertEquals('Bill clinton', $documents[0]['dots.name']); + } + /** * @depends testAttributeCaseInsensitivity */ @@ -834,7 +895,6 @@ public function testGetDocumentSelect(Document $document): Document return $document; } - /** * @depends testCreateDocument */ @@ -845,7 +905,6 @@ public function testFulltextIndexWithInteger(): void static::getDatabase()->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer']); } - /** * @depends testCreateDocument */ @@ -1359,27 +1418,25 @@ public function testFindFloat(): void public function testFindContains(): void { - /** - * Array contains condition - */ - if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { - $documents = static::getDatabase()->find('movies', [ - Query::contains('generes', ['comics']) - ]); + if (!$this->getDatabase()->getAdapter()->getSupportForQueryContains()) { + $this->expectNotToPerformAssertions(); + return; + } - $this->assertEquals(2, count($documents)); + $documents = static::getDatabase()->find('movies', [ + Query::contains('generes', ['comics']) + ]); - /** - * Array contains OR condition - */ - $documents = static::getDatabase()->find('movies', [ - Query::contains('generes', ['comics', 'kids']), - ]); + $this->assertEquals(2, count($documents)); - $this->assertEquals(4, count($documents)); - } + /** + * Array contains OR condition + */ + $documents = static::getDatabase()->find('movies', [ + Query::contains('generes', ['comics', 'kids']), + ]); - $this->assertEquals(true, true); // Test must do an assertion + $this->assertEquals(4, count($documents)); } public function testFindFulltext(): void @@ -3869,19 +3926,6 @@ public function testOneToOneOneWayRelationship(): void $library = static::getDatabase()->getDocument('library', 'library2'); $this->assertArrayNotHasKey('person', $library); - // Query related document - $people = static::getDatabase()->find('person', [ - Query::equal('library.name', ['Library 2']) - ]); - - $this->assertEquals(1, \count($people)); - $this->assertEquals( - 'Library 2', - $people[0] - ->getAttribute('library') - ->getAttribute('name') - ); - $people = static::getDatabase()->find('person', [ Query::select(['name']) ]); @@ -4034,13 +4078,6 @@ public function testOneToOneOneWayRelationship(): void $person1->setAttribute('library', $library4) ); - // Query new related document - $people = static::getDatabase()->find('person', [ - Query::equal('library.name', ['Library 4']) - ]); - - $this->assertEquals(1, \count($people)); - // Rename relationship key static::getDatabase()->updateRelationship( collection: 'person', @@ -4363,19 +4400,6 @@ public function testOneToOneTwoWayRelationship(): void $this->assertEquals('city4', $city['$id']); $this->assertArrayNotHasKey('country', $city); - // Query related document - $countries = static::getDatabase()->find('country', [ - Query::equal('city.name', ['Paris']) - ]); - - $this->assertEquals(1, \count($countries)); - $this->assertEquals( - 'Paris', - $countries[0] - ->getAttribute('city') - ->getAttribute('name') - ); - $countries = static::getDatabase()->find('country'); $this->assertEquals(4, \count($countries)); @@ -4568,13 +4592,6 @@ public function testOneToOneTwoWayRelationship(): void $country1->setAttribute('city', $city7) ); - // Query new relationship - $countries = static::getDatabase()->find('country', [ - Query::equal('city.name', ['Copenhagen']) - ]); - - $this->assertEquals(1, \count($countries)); - // Create a new country with no relation static::getDatabase()->createDocument('country', new Document([ '$id' => 'country7', @@ -4593,13 +4610,6 @@ public function testOneToOneTwoWayRelationship(): void $city1->setAttribute('country', 'country7') ); - // Query inverse related document again - $people = static::getDatabase()->find('city', [ - Query::equal('country.name', ['Denmark']) - ]); - - $this->assertEquals(1, \count($people)); - // Rename relationship keys on both sides static::getDatabase()->updateRelationship( 'country', @@ -4948,21 +4958,6 @@ public function testOneToManyOneWayRelationship(): void $album = static::getDatabase()->getDocument('album', 'album2'); $this->assertArrayNotHasKey('artist', $album); - // Query related document - $artists = static::getDatabase()->find('artist', [ - Query::equal('albums.name', ['Album 2']) - ]); - - $this->assertCount(1, $artists); - $this->assertCount(2, $artists[0]['albums']); - - $this->assertEquals( - 'Album 2', - $artists[0] - ->getAttribute('albums')[0] - ->getAttribute('name') - ); - $artists = static::getDatabase()->find('artist'); $this->assertEquals(2, \count($artists)); @@ -5061,14 +5056,6 @@ public function testOneToManyOneWayRelationship(): void $artist1->setAttribute('albums', ['album2']) ); - // Query related document again - $artists = static::getDatabase()->find('artist', [ - Query::equal('albums.name', ['Album 2']) - ]); - - $this->assertEquals(1, \count($artists)); - $this->assertEquals(1, \count($artists[0]['albums'])); - // Update document with new related documents, will remove existing relations static::getDatabase()->updateDocument( 'artist', @@ -5076,14 +5063,6 @@ public function testOneToManyOneWayRelationship(): void $artist1->setAttribute('albums', ['album1', 'album2']) ); - // Query related document again - $artists = static::getDatabase()->find('artist', [ - Query::equal('albums.name', ['Album 2']) - ]); - - $this->assertEquals(1, \count($artists)); - $this->assertEquals(2, \count($artists[0]['albums'])); - // Rename relationship key static::getDatabase()->updateRelationship( 'artist', @@ -5405,20 +5384,6 @@ public function testOneToManyTwoWayRelationship(): void $this->assertEquals('customer4', $customer['$id']); $this->assertArrayNotHasKey('accounts', $customer); - // Query related document - $customers = static::getDatabase()->find('customer', [ - Query::equal('accounts.name', ['Account 2']) - ]); - - $this->assertEquals(1, \count($customers)); - - $this->assertEquals( - 'Account 2', - $customers[0] - ->getAttribute('accounts')[0] - ->getAttribute('name') - ); - $customers = static::getDatabase()->find('customer'); $this->assertEquals(4, \count($customers)); @@ -5565,14 +5530,6 @@ public function testOneToManyTwoWayRelationship(): void $customer1->setAttribute('accounts', ['account2']) ); - // Query related document again - $customers = static::getDatabase()->find('customer', [ - Query::equal('accounts.name', ['Account 2 Updated']) - ]); - - $this->assertEquals(1, \count($customers)); - $this->assertEquals(1, \count($customers[0]['accounts'])); - // Update document with new related document static::getDatabase()->updateDocument( 'customer', @@ -5580,14 +5537,6 @@ public function testOneToManyTwoWayRelationship(): void $customer1->setAttribute('accounts', ['account1', 'account2']) ); - // Query related document again - $customers = static::getDatabase()->find('customer', [ - Query::equal('accounts.name', ['Account 2 Updated']) - ]); - - $this->assertEquals(1, \count($customers)); - $this->assertEquals(2, \count($customers[0]['accounts'])); - // Update inverse document static::getDatabase()->updateDocument( 'account', @@ -5595,21 +5544,6 @@ public function testOneToManyTwoWayRelationship(): void $account2->setAttribute('customer', 'customer2') ); - // Query related document again - $customers = static::getDatabase()->find('customer', [ - Query::equal('accounts.name', ['Account 2 Updated']) - ]); - - $this->assertEquals(1, \count($customers)); - $this->assertEquals(1, \count($customers[0]['accounts'])); - - // Query inverse document again - $customers = static::getDatabase()->find('account', [ - Query::equal('customer.name', ['Customer 2 Updated']) - ]); - - $this->assertEquals(1, \count($customers)); - // Rename relationship keys on both sides static::getDatabase()->updateRelationship( 'customer', @@ -5853,20 +5787,6 @@ public function testManyToOneOneWayRelationship(): void $movie = static::getDatabase()->getDocument('movie', 'movie2'); $this->assertArrayNotHasKey('reviews', $movie); - // Query related document - $reviews = static::getDatabase()->find('review', [ - Query::equal('movie.name', ['Movie 2']) - ]); - - $this->assertEquals(1, \count($reviews)); - - $this->assertEquals( - 'Movie 2', - $reviews[0] - ->getAttribute('movie') - ->getAttribute('name') - ); - $reviews = static::getDatabase()->find('review'); $this->assertEquals(3, \count($reviews)); @@ -5953,13 +5873,6 @@ public function testManyToOneOneWayRelationship(): void $review1->setAttribute('movie', 'movie2') ); - // Query related document again - $reviews = static::getDatabase()->find('review', [ - Query::equal('movie.name', ['Movie 2']) - ]); - - $this->assertEquals(2, \count($reviews)); - // Rename relationship keys on both sides static::getDatabase()->updateRelationship( 'review', @@ -6242,20 +6155,6 @@ public function testManyToOneTwoWayRelationship(): void $this->assertEquals('product4', $products[0]['$id']); $this->assertArrayNotHasKey('store', $products[0]); - // Query related document - $products = static::getDatabase()->find('product', [ - Query::equal('store.name', ['Store 2']) - ]); - - $this->assertEquals(1, \count($products)); - - $this->assertEquals( - 'Store 2', - $products[0] - ->getAttribute('store') - ->getAttribute('name') - ); - $products = static::getDatabase()->find('product'); $this->assertEquals(4, \count($products)); @@ -6399,13 +6298,6 @@ public function testManyToOneTwoWayRelationship(): void $product1->setAttribute('store', 'store2') ); - // Query related document again - $products = static::getDatabase()->find('product', [ - Query::equal('store.name', ['Store 2']) - ]); - - $this->assertEquals(2, \count($products)); - $store1 = static::getDatabase()->getDocument('store', 'store1'); // Update inverse document @@ -6415,21 +6307,6 @@ public function testManyToOneTwoWayRelationship(): void $store1->setAttribute('products', ['product1']) ); - // Query related document again - $stores = static::getDatabase()->find('store', [ - Query::equal('products.name', ['Product 1 Updated']) - ]); - - $this->assertEquals(1, \count($stores)); - $this->assertEquals(1, \count($stores[0]['products'])); - - // Query inverse document again - $products = static::getDatabase()->find('product', [ - Query::equal('store.name', ['Store 2']) - ]); - - $this->assertEquals(1, \count($products)); - $store2 = static::getDatabase()->getDocument('store', 'store2'); // Update inverse document @@ -6439,21 +6316,6 @@ public function testManyToOneTwoWayRelationship(): void $store2->setAttribute('products', ['product1', 'product2']) ); - // Query related document again - $stores = static::getDatabase()->find('store', [ - Query::equal('products.name', ['Product 1 Updated']) - ]); - - $this->assertEquals(1, \count($stores)); - $this->assertEquals(2, \count($stores[0]['products'])); - - // Query inverse document again - $products = static::getDatabase()->find('product', [ - Query::equal('store.name', ['Store 2']) - ]); - - $this->assertEquals(2, \count($products)); - // Rename relationship keys on both sides static::getDatabase()->updateRelationship( 'product', @@ -6668,20 +6530,6 @@ public function testManyToManyOneWayRelationship(): void $library = static::getDatabase()->getDocument('song', 'song2'); $this->assertArrayNotHasKey('songs', $library); - // Query related document - $playlists = static::getDatabase()->find('playlist', [ - Query::equal('songs.name', ['Song 2']) - ]); - - $this->assertEquals(1, \count($playlists)); - - $this->assertEquals( - 'Song 2', - $playlists[0] - ->getAttribute('songs')[0] - ->getAttribute('name') - ); - $playlists = static::getDatabase()->find('playlist'); $this->assertEquals(2, \count($playlists)); @@ -6784,14 +6632,6 @@ public function testManyToManyOneWayRelationship(): void $playlist1->setAttribute('songs', ['song2']) ); - // Query related document again - $playlists = static::getDatabase()->find('playlist', [ - Query::equal('songs.name', ['Song 2']) - ]); - - $this->assertEquals(3, \count($playlists)); - $this->assertEquals(1, \count($playlists[0]['songs'])); - // Rename relationship key static::getDatabase()->updateRelationship( 'playlist', @@ -7069,24 +6909,6 @@ public function testManyToManyTwoWayRelationship(): void $this->assertEquals('student4', $student[0]['$id']); $this->assertArrayNotHasKey('classes', $student[0]); - // Query related document - $students = static::getDatabase()->find('students', [ - Query::equal('classes.name', ['Class 2']) - ]); - - $this->assertEquals(1, \count($students)); - - $this->assertEquals( - 'Class 2', - $students[0] - ->getAttribute('classes')[0] - ->getAttribute('name') - ); - - $students = static::getDatabase()->find('students'); - - $this->assertEquals(4, \count($students)); - // Select related document attributes $student = static::getDatabase()->findOne('students', [ Query::select(['*', 'classes.name']) @@ -7226,15 +7048,6 @@ public function testManyToManyTwoWayRelationship(): void $student1->setAttribute('classes', ['class2']) ); - // Query related document again - $students = static::getDatabase()->find('students', [ - Query::equal('classes.name', ['Class 2 Updated']) - ]); - - $this->assertEquals(2, \count($students)); - $this->assertEquals(1, \count($students[0]['classes'])); - $this->assertEquals(1, \count($students[1]['classes'])); - $class1 = static::getDatabase()->getDocument('classes', 'class1'); // Update inverse document @@ -7244,22 +7057,6 @@ public function testManyToManyTwoWayRelationship(): void $class1->setAttribute('students', ['student1']) ); - // Query related document again - $students = static::getDatabase()->find('students', [ - Query::equal('classes.name', ['Class 2 Updated']) - ]); - - $this->assertEquals(2, \count($students)); - $this->assertEquals(2, \count($students[0]['classes'])); - $this->assertEquals(1, \count($students[1]['classes'])); - - // Query inverse document again - $customers = static::getDatabase()->find('classes', [ - Query::equal('students.name', ['Student 2 Updated']) - ]); - - $this->assertEquals(1, \count($customers)); - // Rename relationship keys on both sides static::getDatabase()->updateRelationship( 'students', @@ -11273,6 +11070,89 @@ public function testEvents(): void }); } + public function testEmptyOperatorValues(): void + { + try { + static::getDatabase()->findOne('documents', [ + Query::equal('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Equal queries require at least one value.', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::search('string', null), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::notEqual('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::lessThan('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::lessThanEqual('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::greaterThan('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::greaterThanEqual('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Query type does not match expected: string', $e->getMessage()); + } + + try { + static::getDatabase()->findOne('documents', [ + Query::contains('string', []), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(Exception::class, $e); + $this->assertEquals('Invalid query: Contains queries require at least one value.', $e->getMessage()); + } + } + public function testLast(): void { $this->expectNotToPerformAssertions(); diff --git a/tests/Database/Validator/DocumentQueriesTest.php b/tests/Database/Validator/DocumentQueriesTest.php new file mode 100644 index 000000000..aac1c235d --- /dev/null +++ b/tests/Database/Validator/DocumentQueriesTest.php @@ -0,0 +1,85 @@ + + */ + protected array $collection = []; + + /** + * @throws Exception + */ + public function setUp(): void + { + $this->collection = [ + '$collection' => ID::custom(Database::METADATA), + '$id' => ID::custom('movies'), + 'name' => 'movies', + 'attributes' => [ + new Document([ + '$id' => 'title', + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'price', + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]) + ] + ]; + } + + public function tearDown(): void + { + } + + /** + * @throws Exception + */ + public function testValidQueries(): void + { + $validator = new DocumentQueries($this->collection['attributes']); + + $queries = [ + 'select(["title"])', + ]; + + $this->assertEquals(true, $validator->isValid($queries)); + + $queries[] = Query::select(['price.relation']); + $this->assertEquals(true, $validator->isValid($queries)); + } + + /** + * @throws Exception + */ + public function testInvalidQueries(): void + { + $validator = new DocumentQueries($this->collection['attributes']); + + $queries = [Query::limit(1)]; // We only accept Select queries + $this->assertEquals(false, $validator->isValid($queries)); + } +} diff --git a/tests/Database/Validator/DocumentsQueriesTest.php b/tests/Database/Validator/DocumentsQueriesTest.php new file mode 100644 index 000000000..e928e6785 --- /dev/null +++ b/tests/Database/Validator/DocumentsQueriesTest.php @@ -0,0 +1,180 @@ + + */ + protected array $collection = []; + + /** + * @throws Exception + */ + public function setUp(): void + { + $this->collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [ + new Document([ + '$id' => 'title', + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'description', + 'key' => 'description', + 'type' => Database::VAR_STRING, + 'size' => 1000000, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'rating', + 'key' => 'rating', + 'type' => Database::VAR_INTEGER, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'price', + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'is_bool', + 'key' => 'is_bool', + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false, + 'signed' => false, + 'array' => false, + 'filters' => [], + ]) + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('testindex2'), + 'type' => 'key', + 'attributes' => [ + 'title', + 'description', + 'price' + ], + 'orders' => [ + 'ASC', + 'DESC' + ], + ]), + new Document([ + '$id' => ID::custom('testindex3'), + 'type' => 'fulltext', + 'attributes' => [ + 'title' + ], + 'orders' => [] + ]), + ], + ]; + } + + public function tearDown(): void + { + } + + /** + * @throws Exception + */ + public function testValidQueries(): void + { + $validator = new Documents($this->collection['attributes'], $this->collection['indexes']); + + $queries = [ + 'equal("description", "Best movie ever")', + 'equal("description", [""])', + 'lessThanEqual("price", 6.50)', + 'lessThan("price", 6.50)', + 'greaterThan("rating", 4)', + 'greaterThan("rating", 0)', + 'greaterThanEqual("rating", 6)', + 'between("price", 1.50, 6.50)', + 'search("title", "SEO")', + 'startsWith("title", "Good")', + 'endsWith("title", "Night")', + 'isNull("title")', + 'isNotNull("title")', + 'cursorAfter("a")', + 'cursorBefore("b")', + 'orderAsc("title")', + 'limit(10)', + 'offset(10)', + ]; + + $queries[] = Query::orderDesc(''); + $this->assertEquals(true, $validator->isValid($queries)); + + $queries = ['equal("is_bool", false)']; + $this->assertEquals(true, $validator->isValid($queries)); + } + + /** + * @throws Exception + */ + public function testInvalidQueries(): void + { + $validator = new Documents($this->collection['attributes'], $this->collection['indexes']); + + $this->assertEquals(false, $validator->isValid(['notEqual("title", ["Iron Man", "Ant Man"])',])); + $this->assertEquals('Invalid query: NotEqual queries require exactly one value.', $validator->getDescription()); + + $queries = ['search("description", "iron")']; + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Searching by attribute "description" requires a fulltext index.', $validator->getDescription()); + + $queries = ['equal("not_found", 4)']; + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Invalid query: Attribute not found in schema: not_found', $validator->getDescription()); + + $queries = ['search("description", "iron")']; + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Searching by attribute "description" requires a fulltext index.', $validator->getDescription()); + + $queries = ['equal("not_found", 4)']; + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Invalid query: Attribute not found in schema: not_found', $validator->getDescription()); + + $queries = ['limit(-1)']; + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Invalid query: Invalid limit: Value must be a valid range between 1 and ' . number_format(PHP_INT_MAX), $validator->getDescription()); + + $queries = ['equal("title", [])']; // empty array + $this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals('Invalid query: Equal queries require at least one value.', $validator->getDescription()); + } +} diff --git a/tests/Database/Validator/IndexedQueriesTest.php b/tests/Database/Validator/IndexedQueriesTest.php new file mode 100644 index 000000000..2e5b5893d --- /dev/null +++ b/tests/Database/Validator/IndexedQueriesTest.php @@ -0,0 +1,175 @@ +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([ + '$id' => 'name', + 'key' => 'name', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ]; + + $indexes = [ + new Document([ + 'type' => Database::INDEX_KEY, + 'attributes' => ['name'], + ]), + new Document([ + '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([Query::cursorAfter(new Document(['$id'=>'asdf']))]), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(['equal("name", "value")']), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([Query::equal('name', ['value'])]), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(['limit(10)']), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([Query::limit(10)]), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(['offset(10)']), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([Query::offset(10)]), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(['orderAsc("name")']), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([Query::orderAsc('name')]), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(['search("name", "value")']), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([Query::search('name', 'value')]), $validator->getDescription()); + } + + public function testMissingIndex(): void + { + $attributes = [ + new Document([ + 'key' => 'name', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ]; + + $indexes = [ + new Document([ + '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()); + } + + public function testTwoAttributesFulltext(): void + { + $attributes = [ + new Document([ + '$id' => 'ft1', + 'key' => 'ft1', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + new Document([ + '$id' => 'ft2', + 'key' => 'ft2', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ]; + + $indexes = [ + new Document([ + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['ft1','ft2'], + ]), + ]; + + $validator = new IndexedQueries( + $attributes, + $indexes, + [ + new Cursor(), + new Filter($attributes), + new Limit(), + new Offset(), + new Order($attributes) + ] + ); + + $this->assertEquals(false, $validator->isValid([Query::search('ft1', 'value')])); + } +} diff --git a/tests/Database/Validator/OrderValidatorTest.php b/tests/Database/Validator/OrderValidatorTest.php deleted file mode 100644 index c10258280..000000000 --- a/tests/Database/Validator/OrderValidatorTest.php +++ /dev/null @@ -1,145 +0,0 @@ - - */ - protected array $schema; - - /** - * @var array - */ - protected array $indexesSchema; - - /** - * @var array> - */ - protected array $attributes = [ - [ - '$id' => 'title', - 'key' => 'title', - 'type' => Database::VAR_STRING, - 'size' => 256, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'description', - 'key' => 'description', - 'type' => Database::VAR_STRING, - 'size' => 1000000, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'rating', - 'key' => 'rating', - 'type' => Database::VAR_INTEGER, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'price', - 'key' => 'price', - 'type' => Database::VAR_FLOAT, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'published', - 'key' => 'published', - 'type' => Database::VAR_BOOLEAN, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'tags', - 'key' => 'tags', - 'type' => Database::VAR_STRING, - 'size' => 55, - 'required' => true, - 'signed' => true, - 'array' => true, - 'filters' => [], - ], - ]; - - /** - * @var array> - */ - protected array $indexes = [ - [ - '$id' => 'index1', - 'type' => Database::INDEX_KEY, - 'attributes' => ['title'], - 'lengths' => [256], - 'orders' => ['ASC'], - ], - [ - '$id' => 'index2', - 'type' => Database::INDEX_KEY, - 'attributes' => ['price'], - 'lengths' => [], - 'orders' => ['DESC'], - ], - [ - '$id' => 'index3', - 'type' => Database::INDEX_KEY, - 'attributes' => ['published'], - 'lengths' => [], - 'orders' => ['DESC'], - ], - ]; - - /** - * @throws \Exception - */ - public function setUp(): void - { - // Query validator expects array - foreach ($this->attributes as $attribute) { - $this->schema[] = new Document($attribute); - } - - // Query validator expects array - foreach ($this->indexes as $index) { - $this->indexesSchema[] = new Document($index); - } - } - - public function tearDown(): void - { - } - - public function testQuery(): void - { - $validator = new OrderAttributes($this->schema, $this->indexesSchema); - - $this->assertEquals(true, $validator->isValid(['$id'])); - $this->assertEquals(true, $validator->isValid(['title'])); - $this->assertEquals(true, $validator->isValid(['published'])); - $this->assertEquals(false, $validator->isValid(['_uid'])); - } -} diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index ba5910be2..20afc1ee3 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -2,321 +2,84 @@ namespace Utopia\Tests\Validator; -use Utopia\Database\Helpers\ID; -use Utopia\Database\Validator\Query; +use Exception; use PHPUnit\Framework\TestCase; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query as DatabaseQuery; use Utopia\Database\Validator\Queries; +use Utopia\Database\Validator\Query\Cursor; +use Utopia\Database\Validator\Query\Filter; +use Utopia\Database\Validator\Query\Limit; +use Utopia\Database\Validator\Query\Offset; +use Utopia\Database\Validator\Query\Order; class QueriesTest extends TestCase { - /** - * @var array - */ - protected array $collection = []; - - /** - * @var array - */ - protected array $queries = []; - - protected ?Query $queryValidator = null; - - /** - * @throws \Exception - */ public function setUp(): void { - $this->collection = [ - '$id' => Database::METADATA, - '$collection' => Database::METADATA, - 'name' => 'movies', - 'attributes' => [ - new Document([ - '$id' => 'title', - 'key' => 'title', - 'type' => Database::VAR_STRING, - 'size' => 256, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'description', - 'key' => 'description', - 'type' => Database::VAR_STRING, - 'size' => 1000000, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'rating', - 'key' => 'rating', - 'type' => Database::VAR_INTEGER, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'price', - 'key' => 'price', - 'type' => Database::VAR_FLOAT, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'published', - 'key' => 'published', - 'type' => Database::VAR_BOOLEAN, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'tags', - 'key' => 'tags', - 'type' => Database::VAR_STRING, - 'size' => 55, - 'required' => true, - 'signed' => true, - 'array' => true, - 'filters' => [], - ]), - ], - 'indexes' => [], - ]; - - $this->queryValidator = new Query($this->collection['attributes']); - - $query1 = 'notEqual("title", ["Iron Man", "Ant Man"])'; - $query2 = 'equal("description", "Best movie ever")'; - - array_push($this->queries, $query1, $query2); - - // Constructor expects array $indexes - // Object property declaration cannot initialize a Document object - // Add array $indexes separately - $index1 = new Document([ - '$id' => ID::custom('testindex'), - 'type' => 'key', - 'attributes' => [ - 'title', - 'description' - ], - 'orders' => [ - 'ASC', - 'DESC' - ], - ]); - - $index2 = new Document([ - '$id' => ID::custom('testindex2'), - 'type' => 'key', - 'attributes' => [ - 'title', - 'description', - 'price' - ], - 'orders' => [ - 'ASC', - 'DESC' - ], - ]); - - $index3 = new Document([ - '$id' => ID::custom('testindex3'), - 'type' => 'fulltext', - 'attributes' => [ - 'title' - ], - 'orders' => [] - ]); - - $index4 = new Document([ - '$id' => 'testindex4', - 'type' => 'key', - 'attributes' => [ - 'description' - ], - 'orders' => [] - ]); - - $this->collection['indexes'] = [$index1, $index2, $index3, $index4]; } public function tearDown(): void { } - /** - * @throws \Exception - */ - public function testQueries(): void + public function testEmptyQueries(): void { - // test for SUCCESS - $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes']); - - $this->assertEquals(true, $validator->isValid($this->queries), $validator->getDescription()); - - $this->queries[] = 'lessThan("price", 6.50)'; - $this->queries[] = 'greaterThanEqual("price", 5.50)'; - $this->assertEquals(true, $validator->isValid($this->queries)); - - $queries = [DatabaseQuery::orderDesc('')]; - $this->assertEquals(true, $validator->isValid($queries), $validator->getDescription()); - - // test for FAILURE + $validator = new Queries(); - $this->queries[] = 'greaterThan("rating", 4)'; - - $this->assertFalse($validator->isValid($this->queries)); - $this->assertEquals("Index not found: title,description,price,rating", $validator->getDescription()); - - // test for queued index - $query1 = 'lessThan("price", 6.50)'; - $query2 = 'notEqual("title", ["Iron Man", "Ant Man"])'; - - $this->queries = [$query1, $query2]; - $this->assertEquals(false, $validator->isValid($this->queries)); - $this->assertEquals("Index not found: price,title", $validator->getDescription()); - - // test fulltext - - $query3 = 'search("description", "iron")'; - $this->queries = [$query3]; - $this->assertEquals(false, $validator->isValid($this->queries)); - $this->assertEquals("Search method requires fulltext index: description", $validator->getDescription()); + $this->assertEquals(true, $validator->isValid([])); } - /** - * @throws \Exception - */ - public function testLooseOrderQueries(): void + public function testInvalidQuery(): void { - $validator = new Queries( - $this->queryValidator, - [ - new Document([ - '$id' => 'title', - 'key' => 'title', - 'type' => Database::VAR_STRING, - 'size' => 256, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'rating', - 'key' => 'rating', - 'type' => Database::VAR_INTEGER, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - new Document([ - '$id' => 'price', - 'key' => 'price', - 'type' => Database::VAR_FLOAT, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ]), - ], - [ - new Document([ - '$id' => 'testindex5', - 'type' => 'key', - 'attributes' => [ - 'title', - 'price', - 'rating' - ], - 'orders' => [] - ]), - new Document([ - '$id' => 'key-price', - 'type' => 'key', - 'attributes' => [ - 'price' - ], - 'orders' => [] - ]) - ], - true, - ); - - // Test for SUCCESS - $this->assertTrue($validator->isValid([ - 'lessThanEqual("price", 6.50)', - 'lessThanEqual("title", "string")', - 'lessThanEqual("rating", 2002)', - ])); + $validator = new Queries(); - $this->assertTrue($validator->isValid([ - 'lessThanEqual("price", 6.50)', - 'lessThanEqual("title", "string")', - 'lessThanEqual("rating", 2002)', - ])); - - $this->assertTrue($validator->isValid([ - 'lessThanEqual("price", 6.50)', - 'lessThanEqual("rating", 2002)', - 'lessThanEqual("title", "string")', - ])); - - $this->assertTrue($validator->isValid([ - 'lessThanEqual("title", "string")', - 'lessThanEqual("price", 6.50)', - 'lessThanEqual("rating", 2002)', - ])); + $this->assertEquals(false, $validator->isValid(["this.is.invalid"])); + } - $this->assertTrue($validator->isValid([ - 'lessThanEqual("title", "string")', - 'lessThanEqual("rating", 2002)', - 'lessThanEqual("price", 6.50)', - ])); + public function testInvalidMethod(): void + { + $validator = new Queries(); + $this->assertEquals(false, $validator->isValid(['equal("attr", "value")'])); - $this->assertTrue($validator->isValid([ - 'lessThanEqual("rating", 2002)', - 'lessThanEqual("title", "string")', - 'lessThanEqual("price", 6.50)', - ])); + $validator = new Queries([new Limit()]); + $this->assertEquals(false, $validator->isValid(['equal("attr", "value")'])); + } - $this->assertTrue($validator->isValid([ - 'lessThanEqual("rating", 2002)', - 'lessThanEqual("price", 6.50)', - 'lessThanEqual("title", "string")', - ])); + public function testInvalidValue(): void + { + $validator = new Queries([new Limit()]); + $this->assertEquals(false, $validator->isValid(['limit(-1)'])); } /** - * @throws \Exception + * @throws Exception */ - public function testIsStrict(): void + public function testValid(): void { - $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes']); - - $this->assertEquals(true, $validator->isStrict()); + $attributes = [ + new Document([ + '$id' => 'name', + 'key' => 'name', + 'type' => Database::VAR_STRING, + 'array' => false, + ]) + ]; - $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes'], false); + $validator = new Queries( + [ + new Cursor(), + new Filter($attributes), + new Limit(), + new Offset(), + new Order($attributes) + ] + ); - $this->assertEquals(false, $validator->isStrict()); + $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()); } } diff --git a/tests/Database/Validator/Query/CursorTest.php b/tests/Database/Validator/Query/CursorTest.php new file mode 100644 index 000000000..f68d2bd3a --- /dev/null +++ b/tests/Database/Validator/Query/CursorTest.php @@ -0,0 +1,32 @@ +assertTrue($validator->isValid(new Query(Query::TYPE_CURSORAFTER, values: ['asdf']))); + $this->assertTrue($validator->isValid(new Query(Query::TYPE_CURSORBEFORE, values: ['asdf']))); + } + + public function testValueFailure(): void + { + $validator = new Cursor(); + + $this->assertFalse($validator->isValid(Query::limit(-1))); + $this->assertEquals('Invalid query', $validator->getDescription()); + $this->assertFalse($validator->isValid(Query::limit(101))); + $this->assertFalse($validator->isValid(Query::offset(-1))); + $this->assertFalse($validator->isValid(Query::offset(5001))); + $this->assertFalse($validator->isValid(Query::equal('attr', ['v']))); + $this->assertFalse($validator->isValid(Query::orderAsc('attr'))); + $this->assertFalse($validator->isValid(Query::orderDesc('attr'))); + } +} diff --git a/tests/Database/Validator/Query/FilterTest.php b/tests/Database/Validator/Query/FilterTest.php new file mode 100644 index 000000000..752dd97a3 --- /dev/null +++ b/tests/Database/Validator/Query/FilterTest.php @@ -0,0 +1,96 @@ +validator = new Filter( + attributes: [ + new Document([ + '$id' => 'attr', + 'key' => 'attr', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ], + ); + } + + public function testSuccess(): void + { + $this->assertTrue($this->validator->isValid(Query::between('attr', '1975-12-06', '2050-12-06'))); + $this->assertTrue($this->validator->isValid(Query::isNotNull('attr'))); + $this->assertTrue($this->validator->isValid(Query::isNull('attr'))); + $this->assertTrue($this->validator->isValid(Query::startsWith('attr', 'super'))); + $this->assertTrue($this->validator->isValid(Query::endsWith('attr', 'man'))); + } + + public function testFailure(): void + { + $this->assertFalse($this->validator->isValid(Query::select(['attr']))); + $this->assertEquals('Invalid query', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid(Query::limit(1))); + $this->assertFalse($this->validator->isValid(Query::limit(0))); + $this->assertFalse($this->validator->isValid(Query::limit(100))); + $this->assertFalse($this->validator->isValid(Query::limit(-1))); + $this->assertFalse($this->validator->isValid(Query::limit(101))); + $this->assertFalse($this->validator->isValid(Query::offset(1))); + $this->assertFalse($this->validator->isValid(Query::offset(0))); + $this->assertFalse($this->validator->isValid(Query::offset(5000))); + $this->assertFalse($this->validator->isValid(Query::offset(-1))); + $this->assertFalse($this->validator->isValid(Query::offset(5001))); + $this->assertFalse($this->validator->isValid(Query::equal('dne', ['v']))); + $this->assertFalse($this->validator->isValid(Query::equal('', ['v']))); + $this->assertFalse($this->validator->isValid(Query::orderAsc('attr'))); + $this->assertFalse($this->validator->isValid(Query::orderDesc('attr'))); + $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSORAFTER, values: ['asdf']))); + $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSORBEFORE, values: ['asdf']))); + } + + public function testTypeMissmatch(): void + { + $this->assertFalse($this->validator->isValid(Query::parse('equal("attr", false)'))); + $this->assertEquals('Query type does not match expected: string', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('equal("attr", null)'))); + $this->assertEquals('Query type does not match expected: string', $this->validator->getDescription()); + } + + public function testEmptyValues(): void + { + $this->assertFalse($this->validator->isValid(Query::parse('notEqual("attr", [])'))); + $this->assertEquals('NotEqual queries require exactly one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('contains("attr", [])'))); + $this->assertEquals('Contains queries require at least one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('equal("attr", [])'))); + $this->assertEquals('Equal queries require at least one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('lessThan("attr", [])'))); + $this->assertEquals('LessThan queries require exactly one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('lessThanEqual("attr", [])'))); + $this->assertEquals('LessThanEqual queries require exactly one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('search("attr", [])'))); + $this->assertEquals('Search queries require exactly one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('greaterThanEqual("attr", [])'))); + $this->assertEquals('GreaterThanEqual queries require exactly one value.', $this->validator->getDescription()); + + $this->assertFalse($this->validator->isValid(Query::parse('greaterThan("attr", [])'))); + $this->assertEquals('GreaterThan queries require exactly one value.', $this->validator->getDescription()); + } +} diff --git a/tests/Database/Validator/Query/LimitTest.php b/tests/Database/Validator/Query/LimitTest.php new file mode 100644 index 000000000..a807a09cd --- /dev/null +++ b/tests/Database/Validator/Query/LimitTest.php @@ -0,0 +1,29 @@ +assertTrue($validator->isValid(Query::limit(1))); + $this->assertTrue($validator->isValid(Query::limit(100))); + } + + public function testValueFailure(): void + { + $validator = new Limit(100); + + $this->assertFalse($validator->isValid(Query::limit(0))); + $this->assertEquals('Invalid limit: Value must be a valid range between 1 and 100', $validator->getDescription()); + $this->assertFalse($validator->isValid(Query::limit(0))); + $this->assertFalse($validator->isValid(Query::limit(-1))); + $this->assertFalse($validator->isValid(Query::limit(101))); + } +} diff --git a/tests/Database/Validator/Query/OffsetTest.php b/tests/Database/Validator/Query/OffsetTest.php new file mode 100644 index 000000000..81d1d60c5 --- /dev/null +++ b/tests/Database/Validator/Query/OffsetTest.php @@ -0,0 +1,32 @@ +assertTrue($validator->isValid(Query::offset(1))); + $this->assertTrue($validator->isValid(Query::offset(0))); + $this->assertTrue($validator->isValid(Query::offset(5000))); + } + + public function testValueFailure(): void + { + $validator = new Offset(5000); + + $this->assertFalse($validator->isValid(Query::offset(-1))); + $this->assertEquals('Invalid offset: Value must be a valid range between 0 and 5,000', $validator->getDescription()); + $this->assertFalse($validator->isValid(Query::offset(5001))); + $this->assertFalse($validator->isValid(Query::equal('attr', ['v']))); + $this->assertFalse($validator->isValid(Query::orderAsc('attr'))); + $this->assertFalse($validator->isValid(Query::orderDesc('attr'))); + $this->assertFalse($validator->isValid(Query::limit(100))); + } +} diff --git a/tests/Database/Validator/Query/OrderTest.php b/tests/Database/Validator/Query/OrderTest.php new file mode 100644 index 000000000..c7a0d7826 --- /dev/null +++ b/tests/Database/Validator/Query/OrderTest.php @@ -0,0 +1,51 @@ +validator = new Order( + attributes: [ + new Document([ + '$id' => 'attr', + 'key' => 'attr', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ], + ); + } + + public function testValueSuccess(): void + { + $this->assertTrue($this->validator->isValid(Query::orderAsc('attr'))); + $this->assertTrue($this->validator->isValid(Query::orderAsc(''))); + $this->assertTrue($this->validator->isValid(Query::orderDesc('attr'))); + $this->assertTrue($this->validator->isValid(Query::orderDesc(''))); + } + + public function testValueFailure(): void + { + $this->assertFalse($this->validator->isValid(Query::limit(-1))); + $this->assertEquals('Invalid query', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid(Query::limit(101))); + $this->assertFalse($this->validator->isValid(Query::offset(-1))); + $this->assertFalse($this->validator->isValid(Query::offset(5001))); + $this->assertFalse($this->validator->isValid(Query::equal('attr', ['v']))); + $this->assertFalse($this->validator->isValid(Query::equal('dne', ['v']))); + $this->assertFalse($this->validator->isValid(Query::equal('', ['v']))); + $this->assertFalse($this->validator->isValid(Query::orderDesc('dne'))); + $this->assertFalse($this->validator->isValid(Query::orderAsc('dne'))); + } +} diff --git a/tests/Database/Validator/Query/SelectTest.php b/tests/Database/Validator/Query/SelectTest.php new file mode 100644 index 000000000..a4972bbed --- /dev/null +++ b/tests/Database/Validator/Query/SelectTest.php @@ -0,0 +1,48 @@ +validator = new Select( + attributes: [ + new Document([ + '$id' => 'attr', + 'key' => 'attr', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + new Document([ + '$id' => 'artist', + 'key' => 'artist', + 'type' => Database::VAR_RELATIONSHIP, + 'array' => false, + ]), + ], + ); + } + + public function testValueSuccess(): void + { + $this->assertTrue($this->validator->isValid(Query::select(['*', 'attr']))); + $this->assertTrue($this->validator->isValid(Query::select(['artist.name']))); + } + + public function testValueFailure(): void + { + $this->assertFalse($this->validator->isValid(Query::limit(1))); + $this->assertEquals('Invalid query', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid(Query::select(['name.artist']))); + } +} diff --git a/tests/Database/Validator/QueryTest.php b/tests/Database/Validator/QueryTest.php index b60f9efe6..76f78c1a5 100644 --- a/tests/Database/Validator/QueryTest.php +++ b/tests/Database/Validator/QueryTest.php @@ -2,100 +2,100 @@ namespace Utopia\Tests\Validator; -use Utopia\Database\Validator\Query; +use Exception; use PHPUnit\Framework\TestCase; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query as DatabaseQuery; +use Utopia\Database\Query; +use Utopia\Database\Validator\Queries\Documents; class QueryTest extends TestCase { /** * @var array */ - protected array $schema; + protected array $attributes; /** - * @var array> + * @throws Exception */ - protected array $attributes = [ - [ - '$id' => 'title', - 'key' => 'title', - 'type' => Database::VAR_STRING, - 'size' => 256, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'description', - 'key' => 'description', - 'type' => Database::VAR_STRING, - 'size' => 1000000, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'rating', - 'key' => 'rating', - 'type' => Database::VAR_INTEGER, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'price', - 'key' => 'price', - 'type' => Database::VAR_FLOAT, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'published', - 'key' => 'published', - 'type' => Database::VAR_BOOLEAN, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'tags', - 'key' => 'tags', - 'type' => Database::VAR_STRING, - 'size' => 55, - 'required' => true, - 'signed' => true, - 'array' => true, - 'filters' => [], - ], - [ - '$id' => 'birthDay', - 'key' => 'birthDay', - 'type' => Database::VAR_DATETIME, - 'size' => 0, - 'required' => false, - 'signed' => false, - 'array' => false, - 'filters' => ['datetime'], - ], - ]; - public function setUp(): void { - // Query validator expects array - foreach ($this->attributes as $attribute) { - $this->schema[] = new Document($attribute); + $attributes = [ + [ + '$id' => 'title', + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'description', + 'key' => 'description', + 'type' => Database::VAR_STRING, + 'size' => 1000000, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'rating', + 'key' => 'rating', + 'type' => Database::VAR_INTEGER, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'price', + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'published', + 'key' => 'published', + 'type' => Database::VAR_BOOLEAN, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'tags', + 'key' => 'tags', + 'type' => Database::VAR_STRING, + 'size' => 55, + 'required' => true, + 'signed' => true, + 'array' => true, + 'filters' => [], + ], + [ + '$id' => 'birthDay', + 'key' => 'birthDay', + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => false, + 'signed' => false, + 'array' => false, + 'filters' => ['datetime'], + ], + ]; + + foreach ($attributes as $attribute) { + $this->attributes[] = new Document($attribute); } } @@ -103,141 +103,212 @@ public function tearDown(): void { } + /** + * @throws Exception + */ public function testQuery(): void { - $validator = new Query($this->schema); - - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('equal("$id", ["Iron Man", "Ant Man"])'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('equal("description", "Best movie ever")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('greaterThan("rating", 4)'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('lessThan("price", 6.50)'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('lessThanEqual("price", 6)'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('contains("tags", "action")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('cursorAfter("docId")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('cursorBefore("docId")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('orderAsc("title")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('orderDesc("title")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('isNull("title")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('isNotNull("title")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('between("price", 1.5, 10.9)'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('between("birthDay","2024-01-01", "2023-01-01")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('startsWith("title", "Fro")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('endsWith("title", "Zen")'))); - $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('select(["title", "description"])'))); + $validator = new Documents($this->attributes, []); + + $this->assertEquals(true, $validator->isValid([Query::parse('equal("$id", ["Iron Man", "Ant Man"])')])); + $this->assertEquals(true, $validator->isValid([Query::parse('equal("$id", "Iron Man")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('equal("description", "Best movie ever")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('greaterThan("rating", 4)')])); + $this->assertEquals(true, $validator->isValid([Query::parse('notEqual("title", ["Iron Man"])')])); + $this->assertEquals(true, $validator->isValid([Query::parse('lessThan("price", 6.50)')])); + $this->assertEquals(true, $validator->isValid([Query::parse('lessThanEqual("price", 6)')])); + $this->assertEquals(true, $validator->isValid([Query::parse('contains("tags", ["action1", "action2"])')])); + $this->assertEquals(true, $validator->isValid([Query::parse('contains("tags", "action1")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('cursorAfter("docId")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('cursorBefore("docId")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('orderAsc("title")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('orderDesc("title")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('isNull("title")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('isNotNull("title")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('between("price", 1.5, 10.9)')])); + $this->assertEquals(true, $validator->isValid([Query::parse('between("birthDay","2024-01-01", "2023-01-01")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('startsWith("title", "Fro")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('endsWith("title", "Zen")')])); + $this->assertEquals(true, $validator->isValid([Query::parse('select(["title", "description"])')])); + $this->assertEquals(true, $validator->isValid([Query::parse('notEqual("title", [""])')])); } + /** + * @throws Exception + */ public function testInvalidMethod(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('eqqual("title", "Iron Man")')); + $this->assertEquals(false, $validator->isValid([Query::parse('eqqual("title", "Iron Man")')])); + $this->assertEquals('Invalid query method: eqqual', $validator->getDescription()); - $this->assertEquals(false, $response); - $this->assertEquals('Query method invalid: eqqual', $validator->getDescription()); + $this->assertEquals(false, $validator->isValid([Query::parse('notEqual("title", ["Iron Man", "Ant Man"])')])); } + /** + * @throws Exception + */ public function testAttributeNotFound(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('equal("name", "Iron Man")')); + $response = $validator->isValid([Query::parse('equal("name", "Iron Man")')]); $this->assertEquals(false, $response); - $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); + $this->assertEquals('Invalid query: Attribute not found in schema: name', $validator->getDescription()); - $response = $validator->isValid(DatabaseQuery::parse('orderAsc("name")')); + $response = $validator->isValid([Query::parse('orderAsc("name")')]); $this->assertEquals(false, $response); - $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); + $this->assertEquals('Invalid query: Attribute not found in schema: name', $validator->getDescription()); } + /** + * @throws Exception + */ public function testAttributeWrongType(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('equal("title", 1776)')); + $response = $validator->isValid([Query::parse('equal("title", 1776)')]); $this->assertEquals(false, $response); - $this->assertEquals('Query type does not match expected: string', $validator->getDescription()); - } - - public function testMethodWrongType(): void - { - $validator = new Query($this->schema); - - $response = $validator->isValid(DatabaseQuery::parse('contains("title", "Iron")')); - - $this->assertEquals(false, $response); - $this->assertEquals('Query method only supported on array attributes: contains', $validator->getDescription()); + $this->assertEquals('Invalid query: Query type does not match expected: string', $validator->getDescription()); } + /** + * @throws Exception + */ public function testQueryDate(): void { - $validator = new Query($this->schema); - $response = $validator->isValid(DatabaseQuery::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')); + $validator = new Documents($this->attributes, []); + $response = $validator->isValid([Query::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')]); $this->assertEquals(true, $response); } + /** + * @throws Exception + */ public function testQueryLimit(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('limit(25)')); + $response = $validator->isValid([Query::parse('limit(25)')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('limit()')); + $response = $validator->isValid([Query::parse('limit()')]); $this->assertEquals(false, $response); - $response = $validator->isValid(DatabaseQuery::parse('limit(-1)')); + $response = $validator->isValid([Query::parse('limit(-1)')]); $this->assertEquals(false, $response); - $response = $validator->isValid(DatabaseQuery::parse('limit(10000)')); + $response = $validator->isValid([Query::parse('limit("aaa")')]); $this->assertEquals(false, $response); } + /** + * @throws Exception + */ public function testQueryOffset(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('offset(25)')); + $response = $validator->isValid([Query::parse('offset(25)')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('offset()')); + $response = $validator->isValid([Query::parse('offset()')]); $this->assertEquals(false, $response); - $response = $validator->isValid(DatabaseQuery::parse('offset(-1)')); + $response = $validator->isValid([Query::parse('offset(-1)')]); $this->assertEquals(false, $response); - $response = $validator->isValid(DatabaseQuery::parse('offset(10000)')); + $response = $validator->isValid([Query::parse('offset("aaa")')]); $this->assertEquals(false, $response); } + /** + * @throws Exception + */ public function testQueryOrder(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('orderAsc("title")')); + $response = $validator->isValid([Query::parse('orderAsc("title")')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('orderAsc("")')); + $response = $validator->isValid([Query::parse('orderAsc("")')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('orderAsc()')); + $response = $validator->isValid([Query::parse('orderAsc()')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('orderAsc("doesNotExist")')); + $response = $validator->isValid([Query::parse('orderAsc("doesNotExist")')]); $this->assertEquals(false, $response); } + /** + * @throws Exception + */ public function testQueryCursor(): void { - $validator = new Query($this->schema); + $validator = new Documents($this->attributes, []); - $response = $validator->isValid(DatabaseQuery::parse('cursorAfter("asdf")')); + $response = $validator->isValid([Query::parse('cursorAfter("asdf")')]); $this->assertEquals(true, $response); - $response = $validator->isValid(DatabaseQuery::parse('cursorAfter()')); + $response = $validator->isValid([Query::parse('cursorAfter()')]); $this->assertEquals(false, $response); } + + /** + * @throws Exception + */ + public function testQueryGetByType(): void + { + $queries = [ + Query::equal('key', ['value']), + Query::select(['attr1', 'attr2']), + Query::cursorBefore(new Document([])), + Query::cursorAfter(new Document([])), + ]; + + $queries = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $this->assertCount(2, $queries); + foreach ($queries as $query) { + $this->assertEquals(true, in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE])); + } + } + + /** + * @throws Exception + */ + public function testQueryEmpty(): void + { + $validator = new Documents($this->attributes, []); + + $response = $validator->isValid([Query::equal('title', [''])]); + $this->assertEquals(true, $response); + + $response = $validator->isValid([Query::equal('published', [false])]); + $this->assertEquals(true, $response); + + $response = $validator->isValid([Query::equal('price', [0])]); + $this->assertEquals(true, $response); + + $response = $validator->isValid([Query::greaterThan('price', 0)]); + $this->assertEquals(true, $response); + + $response = $validator->isValid([Query::greaterThan('published', false)]); + $this->assertEquals(true, $response); + + $response = $validator->isValid([Query::equal('price', [])]); + $this->assertEquals(false, $response); + + $response = $validator->isValid([Query::greaterThan('price', null)]); + $this->assertEquals(false, $response); + + $response = $validator->isValid([Query::isNull('price')]); + $this->assertEquals(true, $response); + } }