From 82c48a83019c6786239dda3c4f5830c64dee25f4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sun, 20 Aug 2023 20:31:44 -0400 Subject: [PATCH 1/4] Fix throwing global exception instead of database exception --- src/Database/Adapter.php | 2 +- src/Database/Database.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 22f3976b3..decfd7db4 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -671,7 +671,7 @@ abstract public function getMaxIndexLength(): int; public static function setTimeout(int $milliseconds): void { if ($milliseconds <= 0) { - throw new Exception('Timeout must be greater than 0'); + throw new DatabaseException('Timeout must be greater than 0'); } self::$timeout = $milliseconds; } diff --git a/src/Database/Database.php b/src/Database/Database.php index 48c2968b9..33b5112fc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4046,7 +4046,7 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu $validator = new Documents($attributes, $indexes); if (!$validator->isValid($queries)) { - throw new Exception($validator->getDescription()); + throw new DatabaseException($validator->getDescription()); } $authorization = new Authorization(self::PERMISSION_READ); From b57f33a624db4cb12ad7e4394ac15b5c02858c64 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sun, 20 Aug 2023 21:33:45 -0400 Subject: [PATCH 2/4] Add query exception --- composer.json | 1 + src/Database/Database.php | 150 ++++++++----------------------- src/Database/Exception/Query.php | 9 ++ src/Database/Query.php | 7 +- 4 files changed, 52 insertions(+), 115 deletions(-) create mode 100644 src/Database/Exception/Query.php diff --git a/composer.json b/composer.json index c28e846a0..429d1e558 100755 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ }, "require": { "ext-pdo": "*", + "ext-mbstring": "*", "php": ">=8.0", "utopia-php/framework": "0.*.*", "utopia-php/cache": "0.8.*", diff --git a/src/Database/Database.php b/src/Database/Database.php index 33b5112fc..88a2d7156 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -12,11 +12,13 @@ use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; +use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; 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\Queries\Document as DocumentValidator; +use Utopia\Database\Validator\Queries\Documents as DocumentsValidator; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Structure; @@ -2177,6 +2179,17 @@ public function getDocument(string $collection, string $id, array $queries = []) $collection = $this->silent(fn () => $this->getCollection($collection)); + if ($collection->isEmpty()) { + throw new DatabaseException("Collection not found"); + } + + $attributes = $collection->getAttribute('attributes', []); + + $validator = new DocumentValidator($attributes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + $relationships = \array_filter( $collection->getAttribute('attributes', []), fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP @@ -4044,9 +4057,9 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu $attributes = $collection->getAttribute('attributes', []); $indexes = $collection->getAttribute('indexes', []); - $validator = new Documents($attributes, $indexes); + $validator = new DocumentsValidator($attributes, $indexes); if (!$validator->isValid($queries)) { - throw new DatabaseException($validator->getDescription()); + throw new QueryException($validator->getDescription()); } $authorization = new Authorization(self::PERMISSION_READ); @@ -4157,8 +4170,6 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu } } - $results = $this->applyNestedQueries($results, $nestedQueries, $relationships); - // Remove internal attributes which are not queried foreach ($queries as $query) { if ($query->getMethod() === Query::TYPE_SELECT) { @@ -4178,112 +4189,6 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu return $results; } - /** - * @param array $results - * @param array $queries - * @param array $relationships - * @return array - */ - private function applyNestedQueries(array $results, array $queries, array $relationships): array - { - foreach ($results as $index => &$node) { - foreach ($queries as $query) { - $path = \explode('.', $query->getAttribute()); - - if (\count($path) == 1) { - continue; - } - - $matched = false; - foreach ($relationships as $relationship) { - if ($relationship->getId() === $path[0]) { - $matched = true; - break; - } - } - - if (!$matched) { - continue; - } - - $value = $node->getAttribute($path[0]); - - $levels = \count($path); - for ($i = 1; $i < $levels; $i++) { - if ($value instanceof Document) { - $value = $value->getAttribute($path[$i]); - } - } - - if (\is_array($value)) { - $values = \array_map(function ($value) use ($path, $levels) { - return $value[$path[$levels - 1]]; - }, $value); - } else { - $values = [$value]; - } - - $matched = false; - foreach ($values as $value) { - switch ($query->getMethod()) { - case Query::TYPE_EQUAL: - foreach ($query->getValues() as $queryValue) { - if ($value === $queryValue) { - $matched = true; - break 2; - } - } - break; - case Query::TYPE_NOT_EQUAL: - $matched = $value !== $query->getValue(); - break; - case Query::TYPE_GREATER: - $matched = $value > $query->getValue(); - break; - case Query::TYPE_GREATER_EQUAL: - $matched = $value >= $query->getValue(); - break; - case Query::TYPE_LESSER: - $matched = $value < $query->getValue(); - break; - case Query::TYPE_LESSER_EQUAL: - $matched = $value <= $query->getValue(); - break; - case Query::TYPE_CONTAINS: - $matched = \in_array($query->getValue(), $value); - break; - case Query::TYPE_SEARCH: - $matched = \str_contains($value, $query->getValue()); - break; - case Query::TYPE_IS_NULL: - $matched = $value === null; - break; - case Query::TYPE_IS_NOT_NULL: - $matched = $value !== null; - break; - case Query::TYPE_BETWEEN: - $matched = $value >= $query->getValues()[0] && $value <= $query->getValues()[1]; - break; - case Query::TYPE_STARTS_WITH: - $matched = \str_starts_with($value, $query->getValue()); - break; - case Query::TYPE_ENDS_WITH: - $matched = \str_ends_with($value, $query->getValue()); - break; - default: - break; - } - } - - if (!$matched) { - unset($results[$index]); - } - } - } - - return \array_values($results); - } - /** * @param string $collection * @param array $queries @@ -4292,7 +4197,10 @@ private function applyNestedQueries(array $results, array $queries, array $relat */ public function findOne(string $collection, array $queries = []): bool|Document { - $results = $this->silent(fn () => $this->find($collection, \array_merge([Query::limit(1)], $queries))); + $results = $this->silent(fn () => $this->find($collection, \array_merge([ + Query::limit(1) + ], $queries))); + $found = \reset($results); $this->trigger(self::EVENT_DOCUMENT_FIND, $found); @@ -4320,6 +4228,14 @@ public function count(string $collection, array $queries = [], ?int $max = null) throw new DatabaseException("Collection not found"); } + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + $validator = new DocumentsValidator($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + $authorization = new Authorization(self::PERMISSION_READ); if ($authorization->isValid($collection->getRead())) { $skipAuth = true; @@ -4328,6 +4244,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $queries = Query::groupByType($queries)['filters']; $queries = self::convertQueries($collection, $queries); + $getCount = fn () => $this->adapter->count($collection->getId(), $queries, $max); $count = $skipAuth ?? false ? Authorization::skip($getCount) : $getCount(); @@ -4357,7 +4274,16 @@ public function sum(string $collection, string $attribute, array $queries = [], throw new DatabaseException("Collection not found"); } + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + $validator = new DocumentsValidator($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + $queries = self::convertQueries($collection, $queries); + $sum = $this->adapter->sum($collection->getId(), $attribute, $queries, $max); $this->trigger(self::EVENT_DOCUMENT_SUM, $sum); diff --git a/src/Database/Exception/Query.php b/src/Database/Exception/Query.php new file mode 100644 index 000000000..58f699d12 --- /dev/null +++ b/src/Database/Exception/Query.php @@ -0,0 +1,9 @@ + Date: Mon, 21 Aug 2023 17:22:52 -0400 Subject: [PATCH 3/4] Fix lint --- src/Database/Query.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index fe01d9370..19a28274e 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -2,7 +2,6 @@ namespace Utopia\Database; -use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Query as QueryException; class Query From 1f33a6e04f8e02e881e9fea71d9ed9b238b54f90 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 21 Aug 2023 19:19:01 -0400 Subject: [PATCH 4/4] Apply suggestions from code review --- src/Database/Database.php | 3 +-- src/Database/Query.php | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index cf22e40d0..e5f48e33a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2180,7 +2180,7 @@ public function getDocument(string $collection, string $id, array $queries = []) $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException("Collection not found"); + throw new DatabaseException('Collection not found'); } $attributes = $collection->getAttribute('attributes', []); @@ -4243,7 +4243,6 @@ public function count(string $collection, array $queries = [], ?int $max = null) $queries = Query::groupByType($queries)['filters']; $queries = self::convertQueries($collection, $queries); - $getCount = fn () => $this->adapter->count($collection->getId(), $queries, $max); $count = $skipAuth ?? false ? Authorization::skip($getCount) : $getCount(); diff --git a/src/Database/Query.php b/src/Database/Query.php index 19a28274e..fa8d75e35 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -200,7 +200,7 @@ public static function parse(string $filter): self $paramsStart = mb_strpos($filter, static::CHAR_PARENTHESES_START); if ($paramsStart === false) { - throw new QueryException("Invalid query"); + throw new QueryException('Invalid query'); } $method = mb_substr($filter, 0, $paramsStart); @@ -211,7 +211,7 @@ public static function parse(string $filter): self // Check for deprecated query syntax if (\str_contains($method, '.')) { - throw new QueryException("Invalid query method"); + throw new QueryException('Invalid query method'); } $currentParam = ""; // We build param here before pushing when it's ended